--- /dev/null 2015-09-02 12:15:01.309625027 +0200 +++ new/test/java/lang/ref/CleanerTest.java 2015-10-02 13:38:12.893442897 +0200 @@ -0,0 +1,412 @@ +/* + * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.lang.ref.Cleaner; +import java.lang.ref.Reference; +import java.lang.ref.PhantomReference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.util.Vector; +import java.util.concurrent.Semaphore; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.testng.Assert; +import org.testng.TestNG; +import org.testng.annotations.Test; + +/* + * @test + * @library /lib/testlibrary + * @run testng/othervm -Xmx16m CleanerTest + */ + +@Test +public class CleanerTest { + // A common CleaningService used by the test for notifications + static final Cleaner COMMON = Cleaner.create(); + + /** + * Test that sequences of the various actions on a Reference + * and on the Cleanable instance have the desired result. + * The test cases are generated for each of phantom, weak and soft + * references. + * The sequence of actions includes all permutations to an initial + * list of actions including clearing the ref and resulting garbage + * collection actions on the reference, explicitly performing + * the cleanup and explicitly clearing the cleaning function. + */ + @Test + @SuppressWarnings("unchecked") + void test1() { + Cleaner cleaner = Cleaner.create(); + + // Individually + generateCases(cleaner, c -> c.clearRef()); + generateCases(cleaner, c -> c.doClean()); + generateCases(cleaner, c -> c.doClear()); + + // Pairs + generateCases(cleaner, c -> c.doClear(), c -> c.doClean()); + + // Triplets + generateCases(cleaner, c -> c.doClear(), c -> c.doClean(), c -> c.clearRef()); + + CleanableCase s = setupPhantom(COMMON, cleaner); + cleaner = null; + Assert.assertTrue(checkCleaned(s.getSemaphore()), "Cleaner cleanup should have occurred"); + + } + + /** + * Generate tests using the runnables for each of phantom, weak, + * and soft references. + * @param cleaner the cleaner + * @param runnables the sequence of actions on the test case + */ + @SuppressWarnings("unchecked") + void generateCases(Cleaner cleaner, Consumer... runnables) { + generateCases(() -> setupPhantom(cleaner, null), runnables.length, runnables); + generateCases(() -> setupWeak(cleaner, null), runnables.length, runnables); + generateCases(() -> setupSoft(cleaner, null), runnables.length, runnables); + } + + /** + * Generate all permutations of the sequence of runnables + * and test each one. + * The permutations are generated using Heap, B.R. (1963) Permutations by Interchanges. + * @param generator the supplier of a CleanableCase + * @param n the first index to interchange + * @param runnables the sequence of actions + */ + @SuppressWarnings("unchecked") + void generateCases(Supplier generator, int n, Consumer ... runnables) { + System.out.printf("Heap n: %d%n", n); + if (n == 1) { + CleanableCase test = generator.get(); + // Apply the sequence of actions on the Ref + for (Consumer c : runnables) { + c.accept(test); + } + verify(test); + } else { + for (int i = 0; i < n - 1; i += 1) { + generateCases(generator, n - 1, runnables); + Consumer t = runnables[n - 1]; + int ndx = ((n & 1) == 0) ? i : 0; + runnables[n - 1] = runnables[ndx]; + runnables[ndx] = t; + } + generateCases(generator, n - 1, runnables); + } + } + + /** + * Verify the test case. + * Any actions directly on the Reference or Cleanable have been executed. + * The CleanableCase under test is given a chance to do the cleanup + * by forcing a GC. + * The result is compared with the expecte result computed + * from the sequence of operations on the Cleanable. + * The Cleanable itself should have been cleanedup. + * + * @param test A CleanableCase containing the references + */ + void verify(CleanableCase test) { + int r = test.expectedResult(); + System.out.printf("Case: %s, expect: %s, events: %s%n", + test.getRef().getClass().getName(), test.eventName(r), test.eventsString()); + + CleanableCase cc = setupPhantom(COMMON, test.getCleanable()); + test.clearCleanable(); // release this hard reference + + boolean result = checkCleaned(test.getSemaphore()); + if (result) { + Assert.assertEquals(r, CleanableCase.EV_CLEAN, "expected cleaning to be done"); + } else { + Assert.assertNotEquals(r, CleanableCase.EV_CLEAN, "unexpected cleaning"); + } + Assert.assertTrue(checkCleaned(cc.getSemaphore()), "The reference to the Cleaner should have been freed"); + } + + /** + * Test that releasing the reference to the Cleaner service allows it to be + * be freed. + */ + @Test + void test2() { + ReferenceQueue queue = new ReferenceQueue<>(); + Cleaner service = Cleaner.create(); + + PhantomReference ref = new PhantomReference<>(service, queue); + System.gc(); + // Clear the Reference to the cleaning service and force a gc. + service = null; + System.gc(); + try { + Reference r = queue.remove(); + Assert.assertEquals(r, ref, "Wrong Reference dequeued"); + } catch (InterruptedException ie) { + System.out.printf("queue.remove Interrupted%n"); + } + + } + + /** + * Check a set of semaphores having been released by cleanup handlers. + * Force a number of GC cycles to give the GC a chance to process + * all the References and for the cleanup functions to be run. + * + * @param semaphore a varargs list of Semaphores + * @return true if all of the semaphores have at least 1 permit, + * false otherwise. + */ + static boolean checkCleaned(Semaphore... semaphore) { + long[] cycles = new long[semaphore.length]; + long total = 0; + for (int cycle = 0; cycle < 20; cycle++) { + for (int i = 0; i < semaphore.length; i++) { + long count = semaphore[i].availablePermits(); + if (count > 0 && cycles[i] == 0) { + System.out.printf(" Cleanable[%d] cleaned in cycle: %d%n", i, cycle); + cycles[i] = cycle; + total += 1; + } + } + + if (total == semaphore.length) { + System.out.printf(" All cleanups done in cycle: %d, total: %d%n", cycle, total); + for (int i = 0; i < semaphore.length; i++) { + long count = semaphore[i].availablePermits(); + Assert.assertEquals(count, 1, "Cleanable invoked more than once, semaphore " + i); + } + return true; // all references freed + } + // Force GC + memoryPressure(); + System.gc(); + } + // Not all objects have been cleaned + + for (int i = 0; i < semaphore.length; i++) { + if (cycles[i] != 0) { + System.out.printf(" Cleanable[%d] cleaned in cycle: %d%n", i, cycles[i]); + } else { + System.out.printf(" Cleanable[%d] not cleaned%n", i); + } + } + + return false; // Failing result + } + + /** + * Create a CleanableCase for a PhantomReference. + * @param cleaner the cleaner to use + * @param obj an object or null to create a new Object + * @return a new CleanableCase preset with the object, cleanup, and semaphore + */ + static CleanableCase setupPhantom(Cleaner cleaner, Object obj) { + if (obj == null) { + obj = new Object(); + } + Semaphore s1 = new Semaphore(0); + Cleaner.Cleanable c1 = cleaner.phantomCleanable(obj, () -> s1.release()); + + return new CleanableCase(new PhantomReference<>(obj, null), c1, s1); + } + + /** + * Create a CleanableCase for a WeakReference. + * @param cleaner the cleaner to use + * @param obj an object or null to create a new Object + * @return a new CleanableCase preset with the object, cleanup, and semaphore + */ + static CleanableCase setupWeak(Cleaner cleaner, Object obj) { + if (obj == null) { + obj = new Object(); + } + Semaphore s1 = new Semaphore(0); + Cleaner.Cleanable c1 = cleaner.weakCleanable(obj, () -> s1.release()); + + return new CleanableCase(new WeakReference<>(obj, null), c1, s1); + } + + /** + * Create a CleanableCase for a SoftReference. + * @param cleaner the cleaner to use + * @param obj an object or null to create a new Object + * @return a new CleanableCase preset with the object, cleanup, and semaphore + */ + static CleanableCase setupSoft(Cleaner cleaner, Object obj) { + if (obj == null) { + obj = new Object(); + } + Semaphore s1 = new Semaphore(0); + Cleaner.Cleanable c1 = cleaner.softCleanable(obj, () -> s1.release()); + + return new CleanableCase(new SoftReference<>(obj, null), c1, s1); + } + + /** + * MemoryPressure allocates memory to force a gc and to clear SoftReferences. + */ + static void memoryPressure() { + Vector root = new Vector<>(); + try { + long free = 0; + while ((free = Runtime.getRuntime().freeMemory()) > 1_000_000) { + long[] extra = new long[1_000_000]; + root.addElement(extra); + } + } catch (OutOfMemoryError mem) { + // ignore + root = null; + } + } + + /** + * CleanableCase encapsulates the objects used for a test. + * The reference to the object is not held directly, + * but in a Reference object that can be cleared. + * The semaphore is used to count whether the cleanup occurred. + * It can be awaited on to determine that the cleanup has occurred. + * It can be checked for non-zero to determine if it was + * invoked or if itwas invoked twice (a bug). + */ + static class CleanableCase { + + private Reference ref; + private Cleaner.Cleanable cleanup; + private Semaphore semaphore; + private int[] events; // Sequence of calls to clean, clear, etc. + private int eventNdx; + public static int EV_UNKNOWN = 0; + public static int EV_CLEAR = 1; + public static int EV_CLEAN = 2; + public static int EV_UNREF = 3; + public static int EV_CLEAR_CLEANUP = 4; + + + CleanableCase(Reference ref, Cleaner.Cleanable cleanup, Semaphore semaphore) { + this.ref = ref; + this.cleanup = cleanup; + this.semaphore = semaphore; + this.events = new int[4]; + this.eventNdx = 0; + } + + public Object getRef() { + return ref; + } + + public void clearRef() { + addEvent(EV_UNREF); + ref.clear(); + } + + public Cleaner.Cleanable getCleanable() { + return cleanup; + } + + public void doClean() { + addEvent(EV_CLEAN); + cleanup.clean(); + } + + public void doClear() { + addEvent(EV_CLEAR); + cleanup.clear(); + } + + public void clearCleanable() { + addEvent(EV_CLEAR_CLEANUP); + cleanup = null; + } + + public Semaphore getSemaphore() { + return semaphore; + } + + public boolean isCleaned() { + return semaphore.availablePermits() != 0; + } + + private synchronized void addEvent(int e) { + events[eventNdx++] = e; + } + + /** + * Computed the expected result from the sequence of events. + * If EV_CLEAR appears before anything else, it is cleared. + * If EV_CLEAN appears before EV_UNREF, then it is cleaned. + * Anything else is Unknown. + * @return EV_CLEAR if the cleanup should occur; + * EV_CLEAN if the cleanup should occur; + * EV_UNKNOWN if it is unknown. + */ + public synchronized int expectedResult() { + // Test if EV_CLEAR appears before anything else + int clearNdx = indexOfEvent(EV_CLEAR); + int cleanNdx = indexOfEvent(EV_CLEAN); + int unrefNdx = indexOfEvent(EV_UNREF); + if (clearNdx < cleanNdx) { + return EV_CLEAR; + } + if (cleanNdx < clearNdx || cleanNdx < unrefNdx) { + return EV_CLEAN; + } + if (unrefNdx < eventNdx) { + return EV_CLEAN; + } + + return EV_UNKNOWN; + } + + private synchronized int indexOfEvent(int e) { + for (int i = 0; i < eventNdx; i++) { + if (events[i] == e) { + return i; + } + } + return eventNdx; + } + + private static final String[] names = + {"UNKNOWN", "EV_CLEAR", "EV_CLEAN", "EV_UNREF", "EV_CLEAR_CLEANUP"}; + + public String eventName(int event) { + return names[event]; + } + + public synchronized String eventsString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < eventNdx; i++) { + sb.append(eventName(events[i])); + sb.append(' '); + } + return sb.toString(); + } + } + +}