/*
* 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 /test/lib/share/classes /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():");
}
}