2586f36120
Reviewed-by: lmesnik, cjplummer, psandoz, mchung, sspitsyn, jpai
572 lines
20 KiB
Java
572 lines
20 KiB
Java
/*
|
|
* Copyright (c) 2015, 2023, 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.util.Objects;
|
|
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.CleanerFactory;
|
|
|
|
import jdk.test.whitebox.WhiteBox;
|
|
|
|
import jdk.test.lib.Utils;
|
|
|
|
import org.testng.Assert;
|
|
import org.testng.TestNG;
|
|
import org.testng.annotations.DataProvider;
|
|
import org.testng.annotations.Test;
|
|
|
|
/*
|
|
* @test
|
|
* @library /lib/testlibrary /test/lib
|
|
* @build jdk.test.whitebox.WhiteBox
|
|
* jdk.test.lib.Utils
|
|
* jdk.test.lib.Asserts
|
|
* jdk.test.lib.JDKToolFinder
|
|
* jdk.test.lib.JDKToolLauncher
|
|
* jdk.test.lib.Platform
|
|
* jdk.test.lib.process.*
|
|
* @modules java.base/jdk.internal.misc
|
|
* java.base/jdk.internal.ref
|
|
* java.management
|
|
* @compile CleanerTest.java
|
|
* @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.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();
|
|
|
|
@DataProvider(name = "cleanerSuppliers")
|
|
public Object[][] factories() {
|
|
Supplier<Cleaner> supplier1 = () -> Cleaner.create();
|
|
Supplier<Cleaner> supplier2 = () -> Cleaner.create(Thread.ofVirtual().factory());
|
|
return new Object[][] { { supplier1 }, { supplier2 } };
|
|
}
|
|
|
|
/**
|
|
* 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(dataProvider = "cleanerSuppliers")
|
|
@SuppressWarnings("unchecked")
|
|
public void testCleanableActions(Supplier<Cleaner> supplier) {
|
|
Cleaner cleaner = supplier.get();
|
|
|
|
// 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(dataProvider = "cleanerSuppliers")
|
|
@SuppressWarnings("unchecked")
|
|
public void testRefSubtypes(Supplier<Cleaner> supplier) {
|
|
Cleaner cleaner = supplier.get();
|
|
|
|
// 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);
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
void generateExceptionCasesInternal(Cleaner cleaner) {
|
|
generateCases(() -> setupPhantomSubclassException(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(dataProvider = "cleanerSuppliers")
|
|
public void testCleanerTermination(Supplier<Cleaner> supplier) {
|
|
ReferenceQueue<Object> queue = new ReferenceQueue<>();
|
|
Cleaner service = supplier.get();
|
|
|
|
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 the function should have been run, otherwise not run
|
|
* @param msg a message describing the cleaning function expected to be run or not run
|
|
*/
|
|
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(200L), TimeUnit.MILLISECONDS)) {
|
|
System.out.printf(" Cleanable cleaned in cycle: %d%n", cycle);
|
|
if (!expectCleaned)
|
|
Assert.fail("Should not have been run: " + msg);
|
|
return;
|
|
}
|
|
} catch (InterruptedException ie) {
|
|
// retry in outer loop
|
|
}
|
|
}
|
|
// Object has not been cleaned
|
|
if (expectCleaned)
|
|
Assert.fail("Should have been run: " + 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 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);
|
|
}
|
|
|
|
/**
|
|
* 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());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify that casting a Cleanup to a Reference is not allowed to
|
|
* get the referent or clear the reference.
|
|
*/
|
|
@Test(dataProvider = "cleanerSuppliers")
|
|
@SuppressWarnings("rawtypes")
|
|
public void testReferentNotAvailable(Supplier<Cleaner> supplier) {
|
|
Cleaner cleaner = supplier.get();
|
|
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
|
|
public void testCleanerFactory() {
|
|
Cleaner cleaner = CleanerFactory.cleaner();
|
|
|
|
Object obj = new Object();
|
|
CleanableCase s = setupPhantom(cleaner, obj);
|
|
obj = null;
|
|
checkCleaned(s.getSemaphore(), true,
|
|
"Object cleaned using internal CleanerFactory.cleaner()");
|
|
}
|
|
}
|