/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.lang.ref.PhantomReference; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.math.BigInteger; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.ConcurrentHashMap; import java.util.TreeMap; /** * Test that objects get finalized and their references cleared in the right order. * * We maintain a list of nominally MAX_LIVE_OBJS numbered finalizable objects. * We then alternately drop the last 50, and add 50 more. When we see an object finalized * or its reference cleared, we make sure that the preceding objects in its group of 50 * have also had their references cleared. We also perform a number of other more * straightforward checks, such as ensuring that all references are eventually cleared, * and all objects are finalized. */ public class Main { // TODO(b/216481630) Enable CHECK_PHANTOM_REFS. This currently occasionally reports a few // PhantomReferences as not enqueued. If this report is correct, this needs to be tracked // down and fixed. static final boolean CHECK_PHANTOM_REFS = false; static final int MAX_LIVE_OBJS = 150; static final int DROP_OBJS = 50; // Number of linked objects dropped in each batch. static final int MIN_LIVE_OBJS = MAX_LIVE_OBJS - DROP_OBJS; static final int TOTAL_OBJS = 200_000; // Allocate this many finalizable objects in total. static final boolean REPORT_DROPS = false; static volatile boolean pleaseStop; AtomicInteger totalFinalized = new AtomicInteger(0); int maxDropped = 0; int liveObjects = 0; // Number of next finalizable object to be allocated. int nextAllocated = 0; // List of finalizable objects in descending order. We add to the front and drop // from the rear. FinalizableObject listHead; // A possibly incomplete list of FinalizableObject indices that were finalized, but // have yet to be checked for consistency with reference processing. ArrayBlockingQueue finalized = new ArrayBlockingQueue<>(20_000); // Maps from object number to Reference; Cleared references are deleted when queues are // processed. TreeMap weakRefs = new TreeMap<>(); ConcurrentHashMap phantomRefs = new ConcurrentHashMap<>(); class FinalizableObject { int n; FinalizableObject next; FinalizableObject(int num, FinalizableObject nextObj) { n = num; next = nextObj; } protected void finalize() { if (!inPhantomRefs(n)) { System.out.println("PhantomRef enqueued before finalizer ran"); } totalFinalized.incrementAndGet(); if (!finalized.offer(n) && REPORT_DROPS) { System.out.println("Dropped finalization of " + n); } } } ReferenceQueue refQueue = new ReferenceQueue<>(); class MyWeakReference extends WeakReference { int n; MyWeakReference(FinalizableObject obj) { super(obj, refQueue); n = obj.n; } }; class MyPhantomReference extends PhantomReference { int n; MyPhantomReference(FinalizableObject obj) { super(obj, refQueue); n = obj.n; } } boolean inPhantomRefs(int n) { MyPhantomReference ref = phantomRefs.get(n); if (ref == null) { return false; } if (ref.n != n) { System.out.println("phantomRef retrieval failed"); } return true; } void CheckOKToClearWeak(int num) { if (num > maxDropped) { System.out.println("WeakRef to live object " + num + " was cleared/enqueued."); } int batchEnd = (num / DROP_OBJS + 1) * DROP_OBJS; for (MyWeakReference wr : weakRefs.subMap(num + 1, batchEnd).values()) { if (wr.n <= num || wr.n / DROP_OBJS != num / DROP_OBJS) { throw new AssertionError("MyWeakReference logic error!"); } // wr referent was dropped in same batch and precedes it in list. if (wr.get() != null) { // This violates the WeakReference spec, and can result in strong references // to objects that have been cleaned. System.out.println("WeakReference to " + wr.n + " was erroneously cleared after " + num); } } } void CheckOKToClearPhantom(int num) { if (num > maxDropped) { System.out.println("PhantomRef to live object " + num + " was enqueued."); } MyWeakReference wr = weakRefs.get(num); if (wr != null && wr.get() != null) { System.out.println("PhantomRef cleared before WeakRef for " + num); } } void emptyAndCheckQueues() { // Check recently finalized objects for consistency with cleared references. while (true) { Integer num = finalized.poll(); if (num == null) { break; } MyWeakReference wr = weakRefs.get(num); if (wr != null) { if (wr.n != num) { System.out.println("Finalization logic error!"); } if (wr.get() != null) { System.out.println("Finalizing object with uncleared reference"); } } CheckOKToClearWeak(num); } // Check recently enqueued references for consistency. while (true) { Reference ref = (Reference) refQueue.poll(); if (ref == null) { break; } if (ref instanceof MyWeakReference) { MyWeakReference wr = (MyWeakReference) ref; if (wr.get() != null) { System.out.println("WeakRef " + wr.n + " enqueued but not cleared"); } CheckOKToClearWeak(wr.n); if (weakRefs.remove(Integer.valueOf(wr.n)) != ref) { System.out.println("Missing WeakReference: " + wr.n); } } else if (ref instanceof MyPhantomReference) { MyPhantomReference pr = (MyPhantomReference) ref; CheckOKToClearPhantom(pr.n); if (phantomRefs.remove(Integer.valueOf(pr.n)) != ref) { System.out.println("Missing PhantomReference: " + pr.n); } } else { System.out.println("Found unrecognized reference in queue"); } } } /** * Add n objects to the head of the list. These will be assigned the next n consecutive * numbers after the current head of the list. */ void addObjects(int n) { for (int i = 0; i < n; ++i) { int me = nextAllocated++; listHead = new FinalizableObject(me, listHead); weakRefs.put(me, new MyWeakReference(listHead)); phantomRefs.put(me, new MyPhantomReference(listHead)); } liveObjects += n; } /** * Drop n finalizable objects from the tail of the list. These are the lowest-numbered objects * in the list. */ void dropObjects(int n) { FinalizableObject list = listHead; FinalizableObject last = null; if (n > liveObjects) { System.out.println("Removing too many elements"); } if (liveObjects == n) { maxDropped = list.n; listHead = null; } else { final int skip = liveObjects - n; for (int i = 0; i < skip; ++i) { last = list; list = list.next; } int expected = nextAllocated - skip - 1; if (list.n != expected) { System.out.println("dropObjects found " + list.n + " but expected " + expected); } maxDropped = expected; last.next = null; } liveObjects -= n; } void testLoop() { System.out.println("Starting"); addObjects(MIN_LIVE_OBJS); final int ITERS = (TOTAL_OBJS - MIN_LIVE_OBJS) / DROP_OBJS; for (int i = 0; i < ITERS; ++i) { addObjects(DROP_OBJS); if (liveObjects != MAX_LIVE_OBJS) { System.out.println("Unexpected live object count"); } dropObjects(DROP_OBJS); if (i % 100 == 0) { // Make sure we don't fall too far behind, otherwise we may run out of memory. System.runFinalization(); } emptyAndCheckQueues(); } dropObjects(MIN_LIVE_OBJS); if (liveObjects != 0 || listHead != null) { System.out.println("Unexpected live objecs at end"); } if (maxDropped != TOTAL_OBJS - 1) { System.out.println("Unexpected dropped object count: " + maxDropped); } for (int i = 0; i < 2; ++i) { Runtime.getRuntime().gc(); System.runFinalization(); emptyAndCheckQueues(); } if (!weakRefs.isEmpty()) { System.out.println("Weak Reference map nonempty size = " + weakRefs.size()); } if (CHECK_PHANTOM_REFS && !phantomRefs.isEmpty()) { try { Thread.sleep(500); } catch (InterruptedException e) { System.out.println("Unexpected interrupt"); } if (!phantomRefs.isEmpty()) { System.out.println("Phantom Reference map nonempty size = " + phantomRefs.size()); System.out.print("First elements:"); int i = 0; for (MyPhantomReference pr : phantomRefs.values()) { System.out.print(" " + pr.n); if (++i > 10) { break; } } System.out.println(""); } } if (totalFinalized.get() != TOTAL_OBJS) { System.out.println("Finalized only " + totalFinalized + " objects"); } } static Runnable causeGCs = new Runnable() { public void run() { // Allocate a lot. BigInteger counter = BigInteger.ZERO; while (!pleaseStop) { counter = counter.add(BigInteger.TEN); } // Look at counter to reduce chance of optimizing out the allocation. if (counter.longValue() % 10 != 0) { System.out.println("Bad causeGCs counter value: " + counter); } } }; public static void main(String[] args) throws Exception { Main theTest = new Main(); Thread gcThread = new Thread(causeGCs); gcThread.setDaemon(true); // Terminate if main thread dies. gcThread.start(); theTest.testLoop(); pleaseStop = true; gcThread.join(); System.out.println("Finished"); } }