/* * Copyright (c) 2015, 2016, 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.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Supplier; import jdk.internal.ref.PhantomCleanable; import jdk.internal.ref.WeakCleanable; import jdk.internal.ref.SoftCleanable; import jdk.internal.ref.CleanerFactory; import sun.hotspot.WhiteBox; import jdk.test.lib.Utils; import org.testng.Assert; import org.testng.TestNG; import org.testng.annotations.Test; /* * @test * @library /lib/testlibrary /test/lib * @build sun.hotspot.WhiteBox * @build jdk.test.lib.Utils * @modules java.base/jdk.internal * java.base/jdk.internal.misc * java.base/jdk.internal.ref * java.management * @run main ClassFileInstaller sun.hotspot.WhiteBox * @run testng/othervm * -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. * -verbose:gc CleanerTest */ @Test public class CleanerTest { // A common CleaningService used by the test for notifications static final Cleaner COMMON = CleanerFactory.cleaner(); // Access to WhiteBox utilities static final WhiteBox whitebox = WhiteBox.getWhiteBox(); /** * 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 and explicitly performing * the cleaning action. */ @Test @SuppressWarnings("unchecked") void testCleanableActions() { Cleaner cleaner = Cleaner.create(); // Individually generateCases(cleaner, c -> c.clearRef()); generateCases(cleaner, c -> c.doClean()); // Pairs generateCases(cleaner, c -> c.doClean(), c -> c.clearRef()); CleanableCase s = setupPhantom(COMMON, cleaner); cleaner = null; checkCleaned(s.getSemaphore(), true, "Cleaner was cleaned:"); } /** * Test the jdk.internal.misc APIs with 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 action. */ @Test @SuppressWarnings("unchecked") void testRefSubtypes() { Cleaner cleaner = Cleaner.create(); // Individually generateCasesInternal(cleaner, c -> c.clearRef()); generateCasesInternal(cleaner, c -> c.doClean()); generateCasesInternal(cleaner, c -> c.doClear()); // Pairs generateCasesInternal(cleaner, c -> c.doClear(), c -> c.doClean()); // Triplets generateCasesInternal(cleaner, c -> c.doClear(), c -> c.doClean(), c -> c.clearRef()); generateExceptionCasesInternal(cleaner); CleanableCase s = setupPhantom(COMMON, cleaner); cleaner = null; checkCleaned(s.getSemaphore(), true, "Cleaner was cleaned:"); } /** * 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<CleanableCase>... runnables) { generateCases(() -> setupPhantom(cleaner, null), runnables.length, runnables); } @SuppressWarnings("unchecked") void generateCasesInternal(Cleaner cleaner, Consumer<CleanableCase>... runnables) { generateCases(() -> setupPhantomSubclass(cleaner, null), runnables.length, runnables); generateCases(() -> setupWeakSubclass(cleaner, null), runnables.length, runnables); generateCases(() -> setupSoftSubclass(cleaner, null), runnables.length, runnables); } @SuppressWarnings("unchecked") void generateExceptionCasesInternal(Cleaner cleaner) { generateCases(() -> setupPhantomSubclassException(cleaner, null), 1, c -> c.clearRef()); generateCases(() -> setupWeakSubclassException(cleaner, null), 1, c -> c.clearRef()); generateCases(() -> setupSoftSubclassException(cleaner, null), 1, c -> c.clearRef()); } /** * 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<CleanableCase> generator, int n, Consumer<CleanableCase> ... runnables) { if (n == 1) { CleanableCase test = generator.get(); try { verifyGetRef(test); // Apply the sequence of actions on the Ref for (Consumer<CleanableCase> c : runnables) { c.accept(test); } verify(test); } catch (Exception e) { Assert.fail(test.toString(), e); } } else { for (int i = 0; i < n - 1; i += 1) { generateCases(generator, n - 1, runnables); Consumer<CleanableCase> 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 expected 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) { System.out.println(test); int r = test.expectedResult(); CleanableCase cc = setupPhantom(COMMON, test.getCleanable()); test.clearCleanable(); // release this hard reference checkCleaned(test.getSemaphore(), r == CleanableCase.EV_CLEAN, "Cleanable was cleaned:"); checkCleaned(cc.getSemaphore(), true, "The reference to the Cleanable was freed:"); } /** * Verify that the reference.get works (or not) as expected. * It handles the cases where UnsupportedOperationException is expected. * * @param test the CleanableCase */ void verifyGetRef(CleanableCase test) { Reference<?> r = (Reference) test.getCleanable(); try { Object o = r.get(); Reference<?> expectedRef = test.getRef(); Assert.assertEquals(expectedRef.get(), o, "Object reference incorrect"); if (r.getClass().getName().endsWith("CleanableRef")) { Assert.fail("should not be able to get referent"); } } catch (UnsupportedOperationException uoe) { if (r.getClass().getName().endsWith("CleanableRef")) { // Expected exception } else { Assert.fail("Unexpected exception from subclassed cleanable: " + uoe.getMessage() + ", class: " + r.getClass()); } } } /** * Test that releasing the reference to the Cleaner service allows it to be * be freed. */ @Test void testCleanerTermination() { ReferenceQueue<Object> queue = new ReferenceQueue<>(); Cleaner service = Cleaner.create(); PhantomReference<Object> 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(1000L); Assert.assertNotNull(r, "queue.remove timeout,"); Assert.assertEquals(r, ref, "Wrong Reference dequeued"); } catch (InterruptedException ie) { System.out.printf("queue.remove Interrupted%n"); } } /** * Check a semaphore having been released by cleanup handler. * Force a number of GC cycles to give the GC a chance to process * the Reference and for the cleanup action to be run. * Use a larger number of cycles to wait for an expected cleaning to occur. * * @param semaphore a Semaphore * @param expectCleaned true if cleaning should occur * @param msg a message to explain the error */ static void checkCleaned(Semaphore semaphore, boolean expectCleaned, String msg) { long max_cycles = expectCleaned ? 10 : 3; long cycle = 0; for (; cycle < max_cycles; cycle++) { // Force GC whitebox.fullGC(); try { if (semaphore.tryAcquire(Utils.adjustTimeout(10L), TimeUnit.MILLISECONDS)) { System.out.printf(" Cleanable cleaned in cycle: %d%n", cycle); Assert.assertEquals(true, expectCleaned, msg); return; } } catch (InterruptedException ie) { // retry in outer loop } } // Object has not been cleaned Assert.assertEquals(false, expectCleaned, msg); } /** * 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.register(obj, () -> s1.release()); return new CleanableCase(new PhantomReference<>(obj, null), c1, s1); } /** * 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 setupPhantomSubclass(Cleaner cleaner, Object obj) { if (obj == null) { obj = new Object(); } Semaphore s1 = new Semaphore(0); Cleaner.Cleanable c1 = new PhantomCleanable<Object>(obj, cleaner) { protected void performCleanup() { 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 setupWeakSubclass(Cleaner cleaner, Object obj) { if (obj == null) { obj = new Object(); } Semaphore s1 = new Semaphore(0); Cleaner.Cleanable c1 = new WeakCleanable<Object>(obj, cleaner) { protected void performCleanup() { 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 setupSoftSubclass(Cleaner cleaner, Object obj) { if (obj == null) { obj = new Object(); } Semaphore s1 = new Semaphore(0); Cleaner.Cleanable c1 = new SoftCleanable<Object>(obj, cleaner) { protected void performCleanup() { s1.release(); } }; return new CleanableCase(new SoftReference<>(obj, null), c1, s1); } /** * 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 setupPhantomSubclassException(Cleaner cleaner, Object obj) { if (obj == null) { obj = new Object(); } Semaphore s1 = new Semaphore(0); Cleaner.Cleanable c1 = new PhantomCleanable<Object>(obj, cleaner) { protected void performCleanup() { s1.release(); throw new RuntimeException("Exception thrown to cleaner thread"); } }; return new CleanableCase(new PhantomReference<>(obj, null), c1, s1, true); } /** * 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 setupWeakSubclassException(Cleaner cleaner, Object obj) { if (obj == null) { obj = new Object(); } Semaphore s1 = new Semaphore(0); Cleaner.Cleanable c1 = new WeakCleanable<Object>(obj, cleaner) { protected void performCleanup() { s1.release(); throw new RuntimeException("Exception thrown to cleaner thread"); } }; return new CleanableCase(new WeakReference<>(obj, null), c1, s1, true); } /** * 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 setupSoftSubclassException(Cleaner cleaner, Object obj) { if (obj == null) { obj = new Object(); } Semaphore s1 = new Semaphore(0); Cleaner.Cleanable c1 = new SoftCleanable<Object>(obj, cleaner) { protected void performCleanup() { s1.release(); throw new RuntimeException("Exception thrown to cleaner thread"); } }; return new CleanableCase(new SoftReference<>(obj, null), c1, s1, true); } /** * 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 it was invoked twice (a bug). */ static class CleanableCase { private volatile Reference<?> ref; private volatile Cleaner.Cleanable cleanup; private final Semaphore semaphore; private final boolean throwsEx; private final int[] events; // Sequence of calls to clean, clear, etc. private volatile 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<Object> ref, Cleaner.Cleanable cleanup, Semaphore semaphore) { this.ref = ref; this.cleanup = cleanup; this.semaphore = semaphore; this.throwsEx = false; this.events = new int[4]; this.eventNdx = 0; } CleanableCase(Reference<Object> ref, Cleaner.Cleanable cleanup, Semaphore semaphore, boolean throwsEx) { this.ref = ref; this.cleanup = cleanup; this.semaphore = semaphore; this.throwsEx = throwsEx; this.events = new int[4]; this.eventNdx = 0; } public Reference<?> getRef() { return ref; } public void clearRef() { addEvent(EV_UNREF); ref.clear(); } public Cleaner.Cleanable getCleanable() { return cleanup; } public void doClean() { try { addEvent(EV_CLEAN); cleanup.clean(); } catch (RuntimeException ex) { if (!throwsEx) { // unless it is known this case throws an exception, rethrow throw ex; } } } public void doClear() { addEvent(EV_CLEAR); ((Reference)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(); sb.append('['); for (int i = 0; i < eventNdx; i++) { if (i > 0) { sb.append(", "); } sb.append(eventName(events[i])); } sb.append(']'); sb.append(", throwEx: "); sb.append(throwsEx); return sb.toString(); } public String toString() { return String.format("Case: %s, expect: %s, events: %s", getRef().getClass().getName(), eventName(expectedResult()), eventsString()); } } /** * Example using a Cleaner to remove WeakKey references from a Map. */ @Test void testWeakKey() { ConcurrentHashMap<WeakKey<String>, String> map = new ConcurrentHashMap<>(); Cleaner cleaner = Cleaner.create(); String key = new String("foo"); // ensure it is not interned String data = "bar"; map.put(new WeakKey<>(key, cleaner, map), data); WeakKey<String> k2 = new WeakKey<>(key, cleaner, map); Assert.assertEquals(map.get(k2), data, "value should be found in the map"); key = null; System.gc(); Assert.assertNotEquals(map.get(k2), data, "value should not be found in the map"); final long CYCLE_MAX = Utils.adjustTimeout(30L); for (int i = 1; map.size() > 0 && i < CYCLE_MAX; i++) { map.forEach( (k, v) -> System.out.printf(" k: %s, v: %s%n", k, v)); try { Thread.sleep(10L); } catch (InterruptedException ie) {} } Assert.assertEquals(map.size(), 0, "Expected map to be empty;"); cleaner = null; } /** * Test sample class for WeakKeys in Map. * @param <K> A WeakKey of type K */ class WeakKey<K> extends WeakReference<K> { private final int hash; private final ConcurrentHashMap<WeakKey<K>, ?> map; Cleaner.Cleanable cleanable; public WeakKey(K key, Cleaner c, ConcurrentHashMap<WeakKey<K>, ?> map) { super(key); this.hash = key.hashCode(); this.map = map; cleanable = new WeakCleanable<Object>(key, c) { protected void performCleanup() { map.remove(WeakKey.this); } }; } public int hashCode() { return hash; } public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof WeakKey)) return false; K key = get(); if (key == null) return obj == this; return key == ((WeakKey<?>)obj).get(); } public String toString() { return "WeakKey:" + Objects.toString(get() + ", cleanableRef: " + ((Reference)cleanable).get()); } } /** * Verify that casting a Cleanup to a Reference is not allowed to * get the referent or clear the reference. */ @Test @SuppressWarnings("rawtypes") void testReferentNotAvailable() { Cleaner cleaner = Cleaner.create(); Semaphore s1 = new Semaphore(0); Object obj = new String("a new string"); Cleaner.Cleanable c = cleaner.register(obj, () -> s1.release()); Reference r = (Reference) c; try { Object o = r.get(); System.out.printf("r: %s%n", Objects.toString(o)); Assert.fail("should not be able to get the referent from Cleanable"); } catch (UnsupportedOperationException uoe) { // expected } try { r.clear(); Assert.fail("should not be able to clear the referent from Cleanable"); } catch (UnsupportedOperationException uoe) { // expected } obj = null; checkCleaned(s1, true, "reference was cleaned:"); cleaner = null; } /** * Test the Cleaner from the CleanerFactory. */ @Test void testCleanerFactory() { Cleaner cleaner = CleanerFactory.cleaner(); Object obj = new Object(); CleanableCase s = setupPhantom(cleaner, obj); obj = null; checkCleaned(s.getSemaphore(), true, "Object was cleaned using CleanerFactor.cleaner():"); } }