1 /* 2 * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 import java.lang.ref.Cleaner; 25 import java.lang.ref.Reference; 26 import java.lang.ref.PhantomReference; 27 import java.lang.ref.ReferenceQueue; 28 import java.lang.ref.SoftReference; 29 import java.lang.ref.WeakReference; 30 import java.util.Objects; 31 import java.util.concurrent.ConcurrentHashMap; 32 import java.util.concurrent.Semaphore; 33 import java.util.concurrent.TimeUnit; 34 import java.util.function.Consumer; 35 import java.util.function.Supplier; 36 37 import jdk.internal.ref.PhantomCleanable; 38 import jdk.internal.ref.WeakCleanable; 39 import jdk.internal.ref.SoftCleanable; 40 import jdk.internal.ref.CleanerFactory; 41 42 import sun.hotspot.WhiteBox; 43 44 import jdk.test.lib.Utils; 45 46 import org.testng.Assert; 47 import org.testng.TestNG; 48 import org.testng.annotations.Test; 49 50 /* 51 * @test 52 * @library /test/lib/share/classes /lib/testlibrary /test/lib 53 * @build sun.hotspot.WhiteBox 54 * @build jdk.test.lib.Utils 55 * @modules java.base/jdk.internal 56 * java.base/jdk.internal.misc 57 * java.base/jdk.internal.ref 58 * java.management 59 * @run main ClassFileInstaller sun.hotspot.WhiteBox 60 * @run testng/othervm 61 * -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. 62 * -verbose:gc CleanerTest 63 */ 64 65 @Test 66 public class CleanerTest { 67 // A common CleaningService used by the test for notifications 68 static final Cleaner COMMON = CleanerFactory.cleaner(); 69 70 // Access to WhiteBox utilities 71 static final WhiteBox whitebox = WhiteBox.getWhiteBox(); 72 73 /** 74 * Test that sequences of the various actions on a Reference 75 * and on the Cleanable instance have the desired result. 76 * The test cases are generated for each of phantom, weak and soft 77 * references. 78 * The sequence of actions includes all permutations to an initial 79 * list of actions including clearing the ref and resulting garbage 80 * collection actions on the reference and explicitly performing 81 * the cleaning action. 82 */ 83 @Test 84 @SuppressWarnings("unchecked") 85 void testCleanableActions() { 86 Cleaner cleaner = Cleaner.create(); 87 88 // Individually 89 generateCases(cleaner, c -> c.clearRef()); 90 generateCases(cleaner, c -> c.doClean()); 91 92 // Pairs 93 generateCases(cleaner, c -> c.doClean(), c -> c.clearRef()); 94 95 CleanableCase s = setupPhantom(COMMON, cleaner); 96 cleaner = null; 97 checkCleaned(s.getSemaphore(), true, "Cleaner was cleaned:"); 98 } 99 100 /** 101 * Test the jdk.internal.misc APIs with sequences of the various actions 102 * on a Reference and on the Cleanable instance have the desired result. 103 * The test cases are generated for each of phantom, weak and soft 104 * references. 105 * The sequence of actions includes all permutations to an initial 106 * list of actions including clearing the ref and resulting garbage 107 * collection actions on the reference, explicitly performing 108 * the cleanup and explicitly clearing the cleaning action. 109 */ 110 @Test 111 @SuppressWarnings("unchecked") 112 void testRefSubtypes() { 113 Cleaner cleaner = Cleaner.create(); 114 115 // Individually 116 generateCasesInternal(cleaner, c -> c.clearRef()); 117 generateCasesInternal(cleaner, c -> c.doClean()); 118 generateCasesInternal(cleaner, c -> c.doClear()); 119 120 // Pairs 121 generateCasesInternal(cleaner, 122 c -> c.doClear(), c -> c.doClean()); 123 124 // Triplets 125 generateCasesInternal(cleaner, 126 c -> c.doClear(), c -> c.doClean(), c -> c.clearRef()); 127 128 generateExceptionCasesInternal(cleaner); 129 130 CleanableCase s = setupPhantom(COMMON, cleaner); 131 cleaner = null; 132 checkCleaned(s.getSemaphore(), true, "Cleaner was cleaned:"); 133 } 134 135 /** 136 * Generate tests using the runnables for each of phantom, weak, 137 * and soft references. 138 * @param cleaner the cleaner 139 * @param runnables the sequence of actions on the test case 140 */ 141 @SuppressWarnings("unchecked") 142 void generateCases(Cleaner cleaner, Consumer<CleanableCase>... runnables) { 143 generateCases(() -> setupPhantom(cleaner, null), runnables.length, runnables); 144 } 145 146 @SuppressWarnings("unchecked") 147 void generateCasesInternal(Cleaner cleaner, Consumer<CleanableCase>... runnables) { 148 generateCases(() -> setupPhantomSubclass(cleaner, null), 149 runnables.length, runnables); 150 generateCases(() -> setupWeakSubclass(cleaner, null), 151 runnables.length, runnables); 152 generateCases(() -> setupSoftSubclass(cleaner, null), 153 runnables.length, runnables); 154 } 155 156 @SuppressWarnings("unchecked") 157 void generateExceptionCasesInternal(Cleaner cleaner) { 158 generateCases(() -> setupPhantomSubclassException(cleaner, null), 159 1, c -> c.clearRef()); 160 generateCases(() -> setupWeakSubclassException(cleaner, null), 161 1, c -> c.clearRef()); 162 generateCases(() -> setupSoftSubclassException(cleaner, null), 163 1, c -> c.clearRef()); 164 } 165 166 /** 167 * Generate all permutations of the sequence of runnables 168 * and test each one. 169 * The permutations are generated using Heap, B.R. (1963) Permutations by Interchanges. 170 * @param generator the supplier of a CleanableCase 171 * @param n the first index to interchange 172 * @param runnables the sequence of actions 173 */ 174 @SuppressWarnings("unchecked") 175 void generateCases(Supplier<CleanableCase> generator, int n, 176 Consumer<CleanableCase> ... runnables) { 177 if (n == 1) { 178 CleanableCase test = generator.get(); 179 try { 180 verifyGetRef(test); 181 182 // Apply the sequence of actions on the Ref 183 for (Consumer<CleanableCase> c : runnables) { 184 c.accept(test); 185 } 186 verify(test); 187 } catch (Exception e) { 188 Assert.fail(test.toString(), e); 189 } 190 } else { 191 for (int i = 0; i < n - 1; i += 1) { 192 generateCases(generator, n - 1, runnables); 193 Consumer<CleanableCase> t = runnables[n - 1]; 194 int ndx = ((n & 1) == 0) ? i : 0; 195 runnables[n - 1] = runnables[ndx]; 196 runnables[ndx] = t; 197 } 198 generateCases(generator, n - 1, runnables); 199 } 200 } 201 202 /** 203 * Verify the test case. 204 * Any actions directly on the Reference or Cleanable have been executed. 205 * The CleanableCase under test is given a chance to do the cleanup 206 * by forcing a GC. 207 * The result is compared with the expected result computed 208 * from the sequence of operations on the Cleanable. 209 * The Cleanable itself should have been cleanedup. 210 * 211 * @param test A CleanableCase containing the references 212 */ 213 void verify(CleanableCase test) { 214 System.out.println(test); 215 int r = test.expectedResult(); 216 217 CleanableCase cc = setupPhantom(COMMON, test.getCleanable()); 218 test.clearCleanable(); // release this hard reference 219 220 checkCleaned(test.getSemaphore(), 221 r == CleanableCase.EV_CLEAN, 222 "Cleanable was cleaned:"); 223 checkCleaned(cc.getSemaphore(), true, 224 "The reference to the Cleanable was freed:"); 225 } 226 227 /** 228 * Verify that the reference.get works (or not) as expected. 229 * It handles the cases where UnsupportedOperationException is expected. 230 * 231 * @param test the CleanableCase 232 */ 233 void verifyGetRef(CleanableCase test) { 234 Reference<?> r = (Reference) test.getCleanable(); 235 try { 236 Object o = r.get(); 237 Reference<?> expectedRef = test.getRef(); 238 Assert.assertEquals(expectedRef.get(), o, 239 "Object reference incorrect"); 240 if (r.getClass().getName().endsWith("CleanableRef")) { 241 Assert.fail("should not be able to get referent"); 242 } 243 } catch (UnsupportedOperationException uoe) { 244 if (r.getClass().getName().endsWith("CleanableRef")) { 245 // Expected exception 246 } else { 247 Assert.fail("Unexpected exception from subclassed cleanable: " + 248 uoe.getMessage() + ", class: " + r.getClass()); 249 } 250 } 251 } 252 253 /** 254 * Test that releasing the reference to the Cleaner service allows it to be 255 * be freed. 256 */ 257 @Test 258 void testCleanerTermination() { 259 ReferenceQueue<Object> queue = new ReferenceQueue<>(); 260 Cleaner service = Cleaner.create(); 261 262 PhantomReference<Object> ref = new PhantomReference<>(service, queue); 263 System.gc(); 264 // Clear the Reference to the cleaning service and force a gc. 265 service = null; 266 System.gc(); 267 try { 268 Reference<?> r = queue.remove(1000L); 269 Assert.assertNotNull(r, "queue.remove timeout,"); 270 Assert.assertEquals(r, ref, "Wrong Reference dequeued"); 271 } catch (InterruptedException ie) { 272 System.out.printf("queue.remove Interrupted%n"); 273 } 274 } 275 276 /** 277 * Check a semaphore having been released by cleanup handler. 278 * Force a number of GC cycles to give the GC a chance to process 279 * the Reference and for the cleanup action to be run. 280 * Use a larger number of cycles to wait for an expected cleaning to occur. 281 * 282 * @param semaphore a Semaphore 283 * @param expectCleaned true if cleaning should occur 284 * @param msg a message to explain the error 285 */ 286 static void checkCleaned(Semaphore semaphore, boolean expectCleaned, 287 String msg) { 288 long max_cycles = expectCleaned ? 10 : 3; 289 long cycle = 0; 290 for (; cycle < max_cycles; cycle++) { 291 // Force GC 292 whitebox.fullGC(); 293 294 try { 295 if (semaphore.tryAcquire(Utils.adjustTimeout(10L), TimeUnit.MILLISECONDS)) { 296 System.out.printf(" Cleanable cleaned in cycle: %d%n", cycle); 297 Assert.assertEquals(true, expectCleaned, msg); 298 return; 299 } 300 } catch (InterruptedException ie) { 301 // retry in outer loop 302 } 303 } 304 // Object has not been cleaned 305 Assert.assertEquals(false, expectCleaned, msg); 306 } 307 308 /** 309 * Create a CleanableCase for a PhantomReference. 310 * @param cleaner the cleaner to use 311 * @param obj an object or null to create a new Object 312 * @return a new CleanableCase preset with the object, cleanup, and semaphore 313 */ 314 static CleanableCase setupPhantom(Cleaner cleaner, Object obj) { 315 if (obj == null) { 316 obj = new Object(); 317 } 318 Semaphore s1 = new Semaphore(0); 319 Cleaner.Cleanable c1 = cleaner.register(obj, () -> s1.release()); 320 321 return new CleanableCase(new PhantomReference<>(obj, null), c1, s1); 322 } 323 324 /** 325 * Create a CleanableCase for a PhantomReference. 326 * @param cleaner the cleaner to use 327 * @param obj an object or null to create a new Object 328 * @return a new CleanableCase preset with the object, cleanup, and semaphore 329 */ 330 static CleanableCase setupPhantomSubclass(Cleaner cleaner, Object obj) { 331 if (obj == null) { 332 obj = new Object(); 333 } 334 Semaphore s1 = new Semaphore(0); 335 336 Cleaner.Cleanable c1 = new PhantomCleanable<Object>(obj, cleaner) { 337 protected void performCleanup() { 338 s1.release(); 339 } 340 }; 341 342 return new CleanableCase(new PhantomReference<>(obj, null), c1, s1); 343 } 344 /** 345 * Create a CleanableCase for a WeakReference. 346 * @param cleaner the cleaner to use 347 * @param obj an object or null to create a new Object 348 * @return a new CleanableCase preset with the object, cleanup, and semaphore 349 */ 350 static CleanableCase setupWeakSubclass(Cleaner cleaner, Object obj) { 351 if (obj == null) { 352 obj = new Object(); 353 } 354 Semaphore s1 = new Semaphore(0); 355 356 Cleaner.Cleanable c1 = new WeakCleanable<Object>(obj, cleaner) { 357 protected void performCleanup() { 358 s1.release(); 359 } 360 }; 361 362 return new CleanableCase(new WeakReference<>(obj, null), c1, s1); 363 } 364 365 /** 366 * Create a CleanableCase for a SoftReference. 367 * @param cleaner the cleaner to use 368 * @param obj an object or null to create a new Object 369 * @return a new CleanableCase preset with the object, cleanup, and semaphore 370 */ 371 static CleanableCase setupSoftSubclass(Cleaner cleaner, Object obj) { 372 if (obj == null) { 373 obj = new Object(); 374 } 375 Semaphore s1 = new Semaphore(0); 376 377 Cleaner.Cleanable c1 = new SoftCleanable<Object>(obj, cleaner) { 378 protected void performCleanup() { 379 s1.release(); 380 } 381 }; 382 383 return new CleanableCase(new SoftReference<>(obj, null), c1, s1); 384 } 385 386 /** 387 * Create a CleanableCase for a PhantomReference. 388 * @param cleaner the cleaner to use 389 * @param obj an object or null to create a new Object 390 * @return a new CleanableCase preset with the object, cleanup, and semaphore 391 */ 392 static CleanableCase setupPhantomSubclassException(Cleaner cleaner, Object obj) { 393 if (obj == null) { 394 obj = new Object(); 395 } 396 Semaphore s1 = new Semaphore(0); 397 398 Cleaner.Cleanable c1 = new PhantomCleanable<Object>(obj, cleaner) { 399 protected void performCleanup() { 400 s1.release(); 401 throw new RuntimeException("Exception thrown to cleaner thread"); 402 } 403 }; 404 405 return new CleanableCase(new PhantomReference<>(obj, null), c1, s1, true); 406 } 407 408 /** 409 * Create a CleanableCase for a WeakReference. 410 * @param cleaner the cleaner to use 411 * @param obj an object or null to create a new Object 412 * @return a new CleanableCase preset with the object, cleanup, and semaphore 413 */ 414 static CleanableCase setupWeakSubclassException(Cleaner cleaner, Object obj) { 415 if (obj == null) { 416 obj = new Object(); 417 } 418 Semaphore s1 = new Semaphore(0); 419 420 Cleaner.Cleanable c1 = new WeakCleanable<Object>(obj, cleaner) { 421 protected void performCleanup() { 422 s1.release(); 423 throw new RuntimeException("Exception thrown to cleaner thread"); 424 } 425 }; 426 427 return new CleanableCase(new WeakReference<>(obj, null), c1, s1, true); 428 } 429 430 /** 431 * Create a CleanableCase for a SoftReference. 432 * @param cleaner the cleaner to use 433 * @param obj an object or null to create a new Object 434 * @return a new CleanableCase preset with the object, cleanup, and semaphore 435 */ 436 static CleanableCase setupSoftSubclassException(Cleaner cleaner, Object obj) { 437 if (obj == null) { 438 obj = new Object(); 439 } 440 Semaphore s1 = new Semaphore(0); 441 442 Cleaner.Cleanable c1 = new SoftCleanable<Object>(obj, cleaner) { 443 protected void performCleanup() { 444 s1.release(); 445 throw new RuntimeException("Exception thrown to cleaner thread"); 446 } 447 }; 448 449 return new CleanableCase(new SoftReference<>(obj, null), c1, s1, true); 450 } 451 452 /** 453 * CleanableCase encapsulates the objects used for a test. 454 * The reference to the object is not held directly, 455 * but in a Reference object that can be cleared. 456 * The semaphore is used to count whether the cleanup occurred. 457 * It can be awaited on to determine that the cleanup has occurred. 458 * It can be checked for non-zero to determine if it was 459 * invoked or if it was invoked twice (a bug). 460 */ 461 static class CleanableCase { 462 463 private volatile Reference<?> ref; 464 private volatile Cleaner.Cleanable cleanup; 465 private final Semaphore semaphore; 466 private final boolean throwsEx; 467 private final int[] events; // Sequence of calls to clean, clear, etc. 468 private volatile int eventNdx; 469 470 public static int EV_UNKNOWN = 0; 471 public static int EV_CLEAR = 1; 472 public static int EV_CLEAN = 2; 473 public static int EV_UNREF = 3; 474 public static int EV_CLEAR_CLEANUP = 4; 475 476 477 CleanableCase(Reference<Object> ref, Cleaner.Cleanable cleanup, 478 Semaphore semaphore) { 479 this.ref = ref; 480 this.cleanup = cleanup; 481 this.semaphore = semaphore; 482 this.throwsEx = false; 483 this.events = new int[4]; 484 this.eventNdx = 0; 485 } 486 CleanableCase(Reference<Object> ref, Cleaner.Cleanable cleanup, 487 Semaphore semaphore, 488 boolean throwsEx) { 489 this.ref = ref; 490 this.cleanup = cleanup; 491 this.semaphore = semaphore; 492 this.throwsEx = throwsEx; 493 this.events = new int[4]; 494 this.eventNdx = 0; 495 } 496 497 public Reference<?> getRef() { 498 return ref; 499 } 500 501 public void clearRef() { 502 addEvent(EV_UNREF); 503 ref.clear(); 504 } 505 506 public Cleaner.Cleanable getCleanable() { 507 return cleanup; 508 } 509 510 public void doClean() { 511 try { 512 addEvent(EV_CLEAN); 513 cleanup.clean(); 514 } catch (RuntimeException ex) { 515 if (!throwsEx) { 516 // unless it is known this case throws an exception, rethrow 517 throw ex; 518 } 519 } 520 } 521 522 public void doClear() { 523 addEvent(EV_CLEAR); 524 ((Reference)cleanup).clear(); 525 } 526 527 public void clearCleanable() { 528 addEvent(EV_CLEAR_CLEANUP); 529 cleanup = null; 530 } 531 532 public Semaphore getSemaphore() { 533 return semaphore; 534 } 535 536 public boolean isCleaned() { 537 return semaphore.availablePermits() != 0; 538 } 539 540 private synchronized void addEvent(int e) { 541 events[eventNdx++] = e; 542 } 543 544 /** 545 * Computed the expected result from the sequence of events. 546 * If EV_CLEAR appears before anything else, it is cleared. 547 * If EV_CLEAN appears before EV_UNREF, then it is cleaned. 548 * Anything else is Unknown. 549 * @return EV_CLEAR if the cleanup should occur; 550 * EV_CLEAN if the cleanup should occur; 551 * EV_UNKNOWN if it is unknown. 552 */ 553 public synchronized int expectedResult() { 554 // Test if EV_CLEAR appears before anything else 555 int clearNdx = indexOfEvent(EV_CLEAR); 556 int cleanNdx = indexOfEvent(EV_CLEAN); 557 int unrefNdx = indexOfEvent(EV_UNREF); 558 if (clearNdx < cleanNdx) { 559 return EV_CLEAR; 560 } 561 if (cleanNdx < clearNdx || cleanNdx < unrefNdx) { 562 return EV_CLEAN; 563 } 564 if (unrefNdx < eventNdx) { 565 return EV_CLEAN; 566 } 567 568 return EV_UNKNOWN; 569 } 570 571 private synchronized int indexOfEvent(int e) { 572 for (int i = 0; i < eventNdx; i++) { 573 if (events[i] == e) { 574 return i; 575 } 576 } 577 return eventNdx; 578 } 579 580 private static final String[] names = 581 {"UNKNOWN", "EV_CLEAR", "EV_CLEAN", "EV_UNREF", "EV_CLEAR_CLEANUP"}; 582 583 public String eventName(int event) { 584 return names[event]; 585 } 586 587 public synchronized String eventsString() { 588 StringBuilder sb = new StringBuilder(); 589 sb.append('['); 590 for (int i = 0; i < eventNdx; i++) { 591 if (i > 0) { 592 sb.append(", "); 593 } 594 sb.append(eventName(events[i])); 595 } 596 sb.append(']'); 597 sb.append(", throwEx: "); 598 sb.append(throwsEx); 599 return sb.toString(); 600 } 601 602 public String toString() { 603 return String.format("Case: %s, expect: %s, events: %s", 604 getRef().getClass().getName(), 605 eventName(expectedResult()), eventsString()); 606 } 607 } 608 609 610 /** 611 * Example using a Cleaner to remove WeakKey references from a Map. 612 */ 613 @Test 614 void testWeakKey() { 615 ConcurrentHashMap<WeakKey<String>, String> map = new ConcurrentHashMap<>(); 616 Cleaner cleaner = Cleaner.create(); 617 String key = new String("foo"); // ensure it is not interned 618 String data = "bar"; 619 620 map.put(new WeakKey<>(key, cleaner, map), data); 621 622 WeakKey<String> k2 = new WeakKey<>(key, cleaner, map); 623 624 Assert.assertEquals(map.get(k2), data, "value should be found in the map"); 625 key = null; 626 System.gc(); 627 Assert.assertNotEquals(map.get(k2), data, "value should not be found in the map"); 628 629 final long CYCLE_MAX = Utils.adjustTimeout(30L); 630 for (int i = 1; map.size() > 0 && i < CYCLE_MAX; i++) { 631 map.forEach( (k, v) -> System.out.printf(" k: %s, v: %s%n", k, v)); 632 try { 633 Thread.sleep(10L); 634 } catch (InterruptedException ie) {} 635 } 636 Assert.assertEquals(map.size(), 0, "Expected map to be empty;"); 637 cleaner = null; 638 } 639 640 /** 641 * Test sample class for WeakKeys in Map. 642 * @param <K> A WeakKey of type K 643 */ 644 class WeakKey<K> extends WeakReference<K> { 645 private final int hash; 646 private final ConcurrentHashMap<WeakKey<K>, ?> map; 647 Cleaner.Cleanable cleanable; 648 649 public WeakKey(K key, Cleaner c, ConcurrentHashMap<WeakKey<K>, ?> map) { 650 super(key); 651 this.hash = key.hashCode(); 652 this.map = map; 653 cleanable = new WeakCleanable<Object>(key, c) { 654 protected void performCleanup() { 655 map.remove(WeakKey.this); 656 } 657 }; 658 } 659 public int hashCode() { return hash; } 660 661 public boolean equals(Object obj) { 662 if (obj == this) { 663 return true; 664 } 665 if (!(obj instanceof WeakKey)) return false; 666 K key = get(); 667 if (key == null) return obj == this; 668 return key == ((WeakKey<?>)obj).get(); 669 } 670 671 public String toString() { 672 return "WeakKey:" + Objects.toString(get() + ", cleanableRef: " + 673 ((Reference)cleanable).get()); 674 } 675 } 676 677 /** 678 * Verify that casting a Cleanup to a Reference is not allowed to 679 * get the referent or clear the reference. 680 */ 681 @Test 682 @SuppressWarnings("rawtypes") 683 void testReferentNotAvailable() { 684 Cleaner cleaner = Cleaner.create(); 685 Semaphore s1 = new Semaphore(0); 686 687 Object obj = new String("a new string"); 688 Cleaner.Cleanable c = cleaner.register(obj, () -> s1.release()); 689 Reference r = (Reference) c; 690 try { 691 Object o = r.get(); 692 System.out.printf("r: %s%n", Objects.toString(o)); 693 Assert.fail("should not be able to get the referent from Cleanable"); 694 } catch (UnsupportedOperationException uoe) { 695 // expected 696 } 697 698 try { 699 r.clear(); 700 Assert.fail("should not be able to clear the referent from Cleanable"); 701 } catch (UnsupportedOperationException uoe) { 702 // expected 703 } 704 705 obj = null; 706 checkCleaned(s1, true, "reference was cleaned:"); 707 cleaner = null; 708 } 709 710 /** 711 * Test the Cleaner from the CleanerFactory. 712 */ 713 @Test 714 void testCleanerFactory() { 715 Cleaner cleaner = CleanerFactory.cleaner(); 716 717 Object obj = new Object(); 718 CleanableCase s = setupPhantom(cleaner, obj); 719 obj = null; 720 checkCleaned(s.getSemaphore(), true, 721 "Object was cleaned using CleanerFactor.cleaner():"); 722 } 723 }