/* * 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.Reference; import java.lang.ref.WeakReference; import java.lang.ref.SoftReference; import java.math.BigInteger; import java.util.ArrayList; /** * Basic test of WeakReferences with large amounts of memory that's only reachable through * finalizers. Also makes sure that finalizer-reachable data is not collected. * Can easily be modified to time Reference.get() blocking. */ public class Main { static final boolean PRINT_TIMES = false; // true will cause benchmark failure. // Data structures repeatedly allocated in background to trigger GC. // Size of finalizer reachable trees. static final int TREE_HEIGHT = 15; // Trees contain 2^TREE_HEIGHT -1 allocated objects. // Number of finalizable tree-owning objects that exist at one point. static final int N_RESURRECTING_OBJECTS = 10; // Number of short-lived, not finalizer-reachable, objects allocated between trees. static final int N_PLAIN_OBJECTS = 20_000; // Number of SoftReferences to CBTs we allocate. static final int N_SOFTREFS = 10; static final boolean BACKGROUND_GC_THREAD = true; static final int NBATCHES = 10; static final int NREFS = PRINT_TIMES ? 1_000_000 : 300_000; // Multiple of NBATCHES. static final int REFS_PER_BATCH = NREFS / NBATCHES; static volatile boolean pleaseStop = false; // Large array of WeakReferences filled and accessed by tests below. ArrayList> weakRefs = new ArrayList<>(NREFS); /** * Complete binary tree data structure. make(n) takes O(2^n) space. */ static class CBT { CBT left; CBT right; CBT(CBT l, CBT r) { left = l; right = r; } static CBT make(int n) { if (n == 0) { return null; } return new CBT(make(n - 1), make(n - 1)); } /** * Check that path described by bit-vector path has the correct length. */ void check(int n, int path) { CBT current = this; for (int i = 0; i < n; i++, path = path >>> 1) { // Unexpectedly short paths result in NPE. if ((path & 1) == 0) { current = current.left; } else { current = current.right; } } if (current != null) { System.out.println("Complete binary tree path too long"); } } } /** * A finalizable object that refers to O(2^TREE_HEIGHT) otherwise unreachable memory. * When finalized, it creates a new identical object, making sure that one always stays * around. */ static class ResurrectingObject { CBT stuff; ResurrectingObject() { stuff = CBT.make(TREE_HEIGHT); } static ResurrectingObject a[] = new ResurrectingObject[2]; static int i = 0; static synchronized void allocOne() { a[(++i) % 2] = new ResurrectingObject(); // Check the previous one to make it hard to optimize anything out. if (i > 1) { a[(i + 1) % 2].stuff.check(TREE_HEIGHT, i /* weirdly interpreted as path */); } } protected void finalize() { stuff.check(TREE_HEIGHT, 42 /* Some path descriptor */); // Allocate a new one to replace this one. allocOne(); } } void fillWeakRefs() { for (int i = 0; i < NREFS; ++i) { weakRefs.add(null); } } /* * Return maximum observed time in nanos to dereference a WeakReference to an unreachable * object. weakRefs is presumed to be pre-filled to have the correct size. */ long timeUnreachableInner() { long maxNanos = 0; // Fill weakRefs with WeakReferences to unreachable integers, a batch at a time. // Then time and test .get() calls on carefully sampled array entries, some of which // will have been cleared. for (int i = 0; i < NBATCHES; ++i) { for (int j = 0; j < REFS_PER_BATCH; ++j) { weakRefs.set(i * REFS_PER_BATCH + j, new WeakReference(new Integer(i * REFS_PER_BATCH + j))); } try { Thread.sleep(50); } catch (InterruptedException e) { System.out.println("Unexpected exception"); } // Iterate over the filled-in section of weakRefs, but look only at a subset of the // elements, making sure the subsets for different top-level iterations are disjoint. // Otherwise the get() calls here will extend the lifetimes of the referents, and we // may never see any cleared WeakReferences. for (int j = (i + 1) * REFS_PER_BATCH - i - 1; j >= 0; j -= NBATCHES) { WeakReference wr = weakRefs.get(j); if (wr != null) { long startNanos = System.nanoTime(); Integer referent = wr.get(); long totalNanos = System.nanoTime() - startNanos; if (referent == null) { // Optimization to reduce max space use and scanning time. weakRefs.set(j, null); } maxNanos = Math.max(maxNanos, totalNanos); if (referent != null && referent.intValue() != j) { System.out.println("Unexpected referent; expected " + j + " got " + referent.intValue()); } } } } return maxNanos; } /* * Wrapper for the above that also checks that references were reclaimed. * We do this separately to make sure any stack references from the core of the * test are gone. Empirically, we otherwise sometimes see the zeroth WeakReference * not reclaimed. */ long timeUnreachable() { long maxNanos = timeUnreachableInner(); Runtime.getRuntime().gc(); System.runFinalization(); // Presumed to wait for reference clearing. for (int i = 0; i < NREFS; ++i) { if (weakRefs.get(i) != null && weakRefs.get(i).get() != null) { System.out.println("WeakReference to " + i + " wasn't cleared"); } } return maxNanos; } /** * Return maximum observed time in nanos to dereference a WeakReference to a reachable * object. Overwrites weakRefs, which is presumed to have NREFS entries already. */ long timeReachable() { long maxNanos = 0; // Similar to the above, but we use WeakReferences to otherwise reachable objects, // which should thus not get cleared. Integer[] strongRefs = new Integer[NREFS]; for (int i = 0; i < NBATCHES; ++i) { for (int j = i * REFS_PER_BATCH; j < (i + 1) * REFS_PER_BATCH; ++j) { Integer newObj = new Integer(j); strongRefs[j] = newObj; weakRefs.set(j, new WeakReference(newObj)); } for (int j = (i + 1) * REFS_PER_BATCH - 1; j >= 0; --j) { WeakReference wr = weakRefs.get(j); long startNanos = System.nanoTime(); Integer referent = wr.get(); long totalNanos = System.nanoTime() - startNanos; maxNanos = Math.max(maxNanos, totalNanos); if (referent == null) { System.out.println("Unexpectedly cleared referent at " + j); } else if (referent.intValue() != j) { System.out.println("Unexpected reachable referent; expected " + j + " got " + referent.intValue()); } } } Reference.reachabilityFence(strongRefs); return maxNanos; } void runTest() { System.out.println("Starting"); fillWeakRefs(); long unreachableNanos = timeUnreachable(); if (PRINT_TIMES) { System.out.println("Finished timeUnrechable()"); } long reachableNanos = timeReachable(); String unreachableMillis = String. format("%,.3f", ((double) unreachableNanos) / 1_000_000); String reachableMillis = String. format("%,.3f", ((double) reachableNanos) / 1_000_000); if (PRINT_TIMES) { System.out.println( "Max time for WeakReference.get (unreachable): " + unreachableMillis); System.out.println( "Max time for WeakReference.get (reachable): " + reachableMillis); } // Only report extremely egregious pauses to avoid spurious failures. if (unreachableNanos > 10_000_000_000L) { System.out.println("WeakReference.get (unreachable) time unreasonably long"); } if (reachableNanos > 10_000_000_000L) { System.out.println("WeakReference.get (reachable) time unreasonably long"); } } /** * Allocate and GC a lot, while keeping significant amounts of finalizer and * SoftReference-reachable memory around. */ static Runnable allocFinalizable = new Runnable() { public void run() { // Allocate and drop some finalizable objects that take a long time // to mark. Designed to be hard to optimize away. Each of these objects will // build a new one in its finalizer before really going away. ArrayList> softRefs = new ArrayList<>(N_SOFTREFS); for (int i = 0; i < N_SOFTREFS; ++i) { // These should not normally get reclaimed, since we shouldn't run out of // memory. They do increase tracing time. softRefs.add(new SoftReference(CBT.make(TREE_HEIGHT))); } for (int i = 0; i < N_RESURRECTING_OBJECTS; ++i) { ResurrectingObject.allocOne(); } BigInteger counter = BigInteger.ZERO; for (int i = 1; !pleaseStop; ++i) { // Allocate a lot of short-lived objects, using BigIntegers to minimize the chance // of the allocation getting optimized out. This makes things slightly more // realistic, since not all objects will be finalizer reachable. for (int j = 0; j < N_PLAIN_OBJECTS / 2; ++j) { 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 allocFinalizable counter value: " + counter); } // Explicitly collect here, mostly to prevent heap growth. Otherwise we get // ahead of the GC and eventually block on it. Runtime.getRuntime().gc(); if (PRINT_TIMES && i % 100 == 0) { System.out.println("Collected " + i + " times"); } } // To be safe, access softRefs. final CBT sample = softRefs.get(N_SOFTREFS / 2).get(); if (sample != null) { sample.check(TREE_HEIGHT, 47 /* some path descriptor */); } } }; public static void main(String[] args) throws Exception { Main theTest = new Main(); Thread allocThread = null; if (BACKGROUND_GC_THREAD) { allocThread = new Thread(allocFinalizable); allocThread.setDaemon(true); // Terminate if main thread dies. allocThread.start(); } theTest.runTest(); if (BACKGROUND_GC_THREAD) { pleaseStop = true; allocThread.join(); } System.out.println("Finished"); } }