diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index 3e413577a56..a766621c9af 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -5607,6 +5607,17 @@ public void run() { /// /// a caching object or null if caching isn't supported public Object createSoftWeakRef(Object o) { + // STILL A WeakReference, DELIBERATELY, and not an oversight now that ParparVM has + // a real SoftReference with a ranked retention policy behind it. + // + // Switching this method -- and deleting the iOS override, which pins every entry + // in a Hashtable until a memory warning replaces the whole map -- changes the + // lifetime of every decoded image, gradient and resource cache in every app on + // every platform. That wants its own change and its own bisect point, because + // each call site also needs deciding individually rather than mechanically: a + // rebuildable cache wants a SoftReference, while a lifetime tracker such as + // JavascriptContext reads a null extract as PROOF the wrapper was collected and + // would leak, or worse, under one that outlives its referent. return new WeakReference(o); } diff --git a/Ports/CLDC11/src/java/lang/ref/SoftReference.java b/Ports/CLDC11/src/java/lang/ref/SoftReference.java new file mode 100644 index 00000000000..4c242f0d169 --- /dev/null +++ b/Ports/CLDC11/src/java/lang/ref/SoftReference.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, Codename One 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. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.lang.ref; +/// A reference the collector keeps while the referent is being used and memory allows, and +/// clears before the process runs out. +/// +/// This directory is the SUPPORTED API SURFACE, not an implementation: `maven/java-runtime` +/// compiles it and `BytecodeComplianceMojo` indexes the result as the set of types an +/// application is allowed to touch. A class implemented in `vm/JavaAPI` but missing here is +/// therefore invisible to user code -- javac resolves it against the host JDK and the +/// compliance check then rejects it as forbidden API, which is a confusing way to learn a +/// feature was never exported. The bodies are deliberately inert for the same reason the +/// neighbouring stubs are. +public class SoftReference extends java.lang.ref.Reference{ + /// Creates a new soft reference that refers to the given object. + public SoftReference(java.lang.Object ref){ + } + +} diff --git a/scripts/cast-semantics-baseline.txt b/scripts/cast-semantics-baseline.txt index f1754f94c10..abc1f2534b1 100644 --- a/scripts/cast-semantics-baseline.txt +++ b/scripts/cast-semantics-baseline.txt @@ -54,7 +54,7 @@ com/codename1/impl/android/AndroidImplementation#scheduleBackgroundWork(Lcom/cod com/codename1/impl/android/AndroidImplementation#signingCertificatesViaReflection(Landroid/content/pm/PackageManager;Ljava/lang/String;)[Landroid/content/pm/Signature;|cast to [Landroid.content.pm.Signature; inside catch(java.lang.Throwable) com/codename1/impl/android/AndroidImplementation#signingCertificatesViaReflection(Landroid/content/pm/PackageManager;Ljava/lang/String;)[Landroid/content/pm/Signature;|cast to java.lang.Boolean inside catch(java.lang.Throwable) com/codename1/impl/android/AndroidImplementation#vibrate(I)V|cast to android.os.Vibrator inside catch(java.lang.Throwable) -com/codename1/impl/android/AndroidImplementation$46#onReceive(Landroid/content/Context;Landroid/content/Intent;)V|cast to android.content.ComponentName inside catch(java.lang.Throwable) +com/codename1/impl/android/AndroidImplementation$47#onReceive(Landroid/content/Context;Landroid/content/Intent;)V|cast to android.content.ComponentName inside catch(java.lang.Throwable) com/codename1/impl/android/AndroidImplementation$SetCurrentFormImpl#run()V|cast to [Ljava.lang.Class; inside catch(java.lang.Throwable) com/codename1/impl/android/AndroidImplementation$SetCurrentFormImpl#run()V|cast to [Ljava.lang.Object; inside catch(java.lang.Throwable) com/codename1/impl/android/AndroidImplementation$SetCurrentFormImpl#run()V|cast to android.graphics.Bitmap inside catch(java.lang.Throwable) diff --git a/vm/ByteCodeTranslator/src/cn1_globals.h b/vm/ByteCodeTranslator/src/cn1_globals.h index 9b90b5d2709..b12ce93136d 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.h +++ b/vm/ByteCodeTranslator/src/cn1_globals.h @@ -1401,6 +1401,18 @@ static inline JAVA_BOOLEAN cn1InNursery(void* p) { // heap ref store), which thread-pausing structurally cannot. extern volatile int gcSatbActive; extern void cn1SatbEnqueue(JAVA_OBJECT old); +// DECLARED HERE, not beside the write barrier, because that copy sits in the #else of +// the CN1_NURSERY split and these four have callers that are not conditional on it: +// nativeMethods' arraycopy/cloneArray bulk barrier and CN1_REF_LOAD_BEGIN/END. With the +// declarations behind the nursery #else, -DCN1_NURSERY compiled those calls as implicit +// C89 declarations returning int, which clang has rejected outright since C99 became the +// default -- so the nursery build did not compile at all, and nothing noticed because no +// gate builds that arm. Duplicating an extern is legal and keeps the two halves honest. +extern volatile int gcSatbTerminating; +extern JAVA_BOOLEAN cn1SatbBulkBegin(void); +extern void cn1SatbEnqueueRangeLocked(JAVA_ARRAY_OBJECT* refs, int count); +extern void cn1SatbBulkEnd(void); +extern void cn1SatbBulkQuiesce(void); #if defined(CN1_DISABLE_SATB) // Escape hatch to A/B the barrier cost or fall back if a regression appears. When // disabled, gcSatbActive is never armed (see codenameOneGCMark) AND the per-store @@ -1414,6 +1426,26 @@ extern void cn1SatbEnqueue(JAVA_OBJECT old); } } while(0) #endif +// ---- java.lang.ref support ------------------------------------------------- +// A WeakReference's referent is NOT traced by the generated mark function. That +// function calls cn1GcDiscoverReference instead (see ByteCodeClass), handing over +// the addresses of the reference's fields, and the collector decides for itself +// whether the referent lives. +// +// Field pointers rather than the object, because this file is a fixed template +// compiled beside whatever the translator emitted: `struct obj__java_lang_ref_Reference` +// does not exist in a program that never uses a reference, so naming it here would +// break the build for those. The layout stays on the generated side. +// +// `strength` is CN1_REF_WEAK or CN1_REF_SOFT, taken from a field the subclass +// constructor sets. Deliberately not a class-pointer comparison: the dead-code pass +// is entitled to remove a class symbol this file would then fail to link against, +// and a user-written subclass of either would compare unequal to both. +#define CN1_REF_WEAK 0 +#define CN1_REF_SOFT 1 +// cn1TouchAge value written by get_field_java_lang_ref_Reference_objReference on +// every read. The collector turns it back into an age in cycles. +#define CN1_REF_TOUCHED (-1) extern const char* volatile cn1LastNamSetter; // diagnosis: last bracket toucher #ifdef CN1_CONSERVATIVE_GC_ROOTS // The bracket's purpose was to suppress GC interaction while native C code @@ -3073,6 +3105,117 @@ void codenameOneGcFree(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT obj); extern int currentGcMarkValue; extern void gcMarkObject(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT obj, JAVA_BOOLEAN force); +// Drop every soft referent at the next collection, whatever the retention policy would +// otherwise have decided. Called when an allocation has actually failed: SoftReference's +// one hard guarantee is that all of them are cleared before the VM gives up, and the +// retention ladder cannot see that coming on a platform with no per-process budget probe. +extern void cn1RefDropAllSoftReferents(void); +extern void cn1GcDiscoverReference(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT ref, JAVA_BOOLEAN force, + JAVA_OBJECT* referentField, JAVA_INT* touchAgeField, + JAVA_INT* agedCycleField, JAVA_INT strength); + + +// ---- the Reference.get() load barrier -------------------------------------- +// Emitted into get_field_java_lang_ref_Reference_objReference, and the reason the +// clear pass is allowed to run with mutators still going. +// +// A thread whose stack was scanned and released early can pull the referent out of a +// reference and hold it in a local the collector has already walked past. That referent +// is then neither marked nor fresh, which is the one case the sweep's "already marked or +// FRESH" invariant does not cover, so without this it is freed under a live pointer. +// Enqueuing puts it in the snapshot: the trial clear of gcSatbActive finds a non-empty +// log, re-arms, marks it, and the reference is left alone. +// +// THE FILTER IS NOT AN OPTIMIZATION, it is what makes this affordable. cn1SatbEnqueue +// takes a mutex per accepted reference, and get() on a hot cache is called far more often +// than anything the per-store barrier sees -- measured on RefPolicy before this filter +// existed, a 400,000-access run put over 10,000 entries into the log per cycle and drove +// the SATB termination loop into its CN1_SATB_MAX_REOPENS cap on every single cycle, +// which is the collector failing to converge rather than a cost. +// +// It skips exactly the referents the clear pass would refuse to clear anyway: already +// marked this epoch, or fresh and therefore kept by the sweep's grace rule. Deliberately +// STRICTER than the clear pass's own test, which also spares mark == epoch - 1 (last +// cycle's slack): bibopGcEpoch is only exactly equal to currentGcMarkValue once +// cn1BibopBeginGcCycle has published it, and a barrier must not depend on a mirror being +// current. Skipping less is always safe; skipping more is not. +// +// A retained soft reference costs nothing here at all, because its referent is marked as +// an ordinary strong edge by cn1GcDiscoverReference before any get() can reach it. +// REGISTERS AROUND THE LOAD, via the same handshake the bulk copies use, and the +// registration is what the caller must hold ACROSS its load -- hence the awkward shape: +// the accessor calls cn1RefLoadBegin(), loads, calls this, then cn1RefLoadEnd(). +// +// Checking gcSatbActive and then enqueuing is not enough on this path, and the reason it +// is enough for the per-store barrier does not carry over. cn1SatbEnqueue takes a mutex, +// so a thread can pass a flag check and then be delayed long enough for the collector to +// clear the field, finish its empty final take, lower gcSatbTerminating and quiesce -- and +// the entry then lands in a log nothing will ever drain, or is skipped entirely, while the +// sweep frees the referent the caller is about to return. The per-store barrier tolerates +// that window because a reference STORED after the fixpoint is already marked or FRESH and +// the sweep keeps both; a weak REFERENT handed out by get() is neither. +// +// AN OUTER "FAST PATH" FLAG CHECK BREAKS THIS, and did: gating entry to +// cn1SatbBulkBegin() on a prior read of the same flags reintroduces the race one level +// out, because the thread can be descheduled between that read and the registration. The +// whole value of cn1SatbBulkBegin is that it registers FIRST and reports afterwards, so +// nothing may be sampled before it. +// +// With the registration held across the load, a false answer is safe rather than merely +// unlikely: the collector cannot be mid-termination (its quiesce waits for this +// registration), so either no mark is running -- and one starting later scans this thread +// with the value already in a register -- or reference processing is complete, in which +// case a field still holding a pointer was not condemned and its referent is marked. +// The deletion barrier for the referent field, with an ATOMIC load. +// +// CN1_SATB_DELETE next door reads through a plain JAVA_OBJECT volatile*, which is right +// for every ordinary field because nothing else writes them concurrently. The referent is +// the exception: cn1GcProcessReferences stores JAVA_NULL into it atomically from the +// collector while Reference.clear() runs here, so the plain read would leave that pair a +// mixed atomic/non-atomic access -- undefined in C, and the same defect that was fixed for +// the getter and for this setter's own store. Making the store atomic and leaving the +// barrier's read plain fixes half a race. +#if defined(CN1_DISABLE_SATB) +#define CN1_SATB_DELETE_REF(fieldAddr) do { } while(0) +#else +#define CN1_SATB_DELETE_REF(fieldAddr) \ + do { if(__builtin_expect(gcSatbActive, 0)) { \ + JAVA_OBJECT cn1__old = __atomic_load_n((JAVA_OBJECT*)(fieldAddr), __ATOMIC_RELAXED); \ + if(cn1__old != JAVA_NULL && !CN1_IS_TAGGED(cn1__old)) cn1SatbEnqueue(cn1__old); \ + } } while(0) +#endif + +#if defined(CN1_DISABLE_SATB) +#define CN1_REF_LOAD_BEGIN() JAVA_FALSE +#define CN1_REF_LOAD_END() do { } while(0) +#define CN1_SATB_REF_KEEP(active, refVal) do { (void)(active); (void)(refVal); } while(0) +#else +// -DCN1_REF_NO_LOAD_BARRIER compiles the registration and the enqueue out, leaving the +// touch stamp and the load. It is UNSOUND -- it is the arm that measures what the barrier +// costs, not a configuration to ship -- and exists because "is get() too expensive?" has to +// be answered with a number rather than an intuition. +#if defined(CN1_REF_NO_LOAD_BARRIER) +#define CN1_REF_LOAD_BEGIN() JAVA_FALSE +#define CN1_REF_LOAD_END() do { } while(0) +#else +#define CN1_REF_LOAD_BEGIN() cn1SatbBulkBegin() +#define CN1_REF_LOAD_END() cn1SatbBulkEnd() +#endif +#ifdef CN1_GC_CONFORM +extern _Atomic long cn1RefGets; +#define CN1_REF_COUNT_GET() atomic_fetch_add_explicit(&cn1RefGets, 1, memory_order_relaxed) +#else +#define CN1_REF_COUNT_GET() do { } while(0) +#endif +#define CN1_SATB_REF_KEEP(active, refVal) \ + do { CN1_REF_COUNT_GET(); JAVA_OBJECT cn1__r = (refVal); \ + if((active) && cn1__r != JAVA_NULL && !CN1_IS_TAGGED(cn1__r)) { \ + int cn1__m = __atomic_load_n(&cn1__r->__codenameOneGcMark, __ATOMIC_RELAXED); \ + int cn1__e = atomic_load_explicit(&bibopGcEpoch, memory_order_relaxed); \ + if(cn1__m != -1 && cn1__m != cn1__e) cn1SatbEnqueue(cn1__r); \ + } } while(0) +#endif + extern void gcMarkArrayObject(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT obj, JAVA_BOOLEAN force); extern JAVA_BOOLEAN removeObjectFromHeapCollection(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT o); diff --git a/vm/ByteCodeTranslator/src/cn1_globals.m b/vm/ByteCodeTranslator/src/cn1_globals.m index 4bb0caee0fa..c4a93106b1a 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.m +++ b/vm/ByteCodeTranslator/src/cn1_globals.m @@ -104,6 +104,17 @@ // CN1_GC_FAULT=earlyfree restores the pre-fix O(1) page-reclaim bound // (gcLastMarkedEpoch != V), which frees slots the per-slot walk would keep. int cn1GcFaultEarlyFree = 0; +// CN1_GC_FAULT=refnoclear skips the clear for a referent the sweep is about to free, so a +// reachable Reference is left holding a pointer into reclaimed memory. That is the exact +// hazard this feature's whole risk reduces to, and until cn1GcDiscoverReference routed the +// referent to cn1GcVerifyChild the verifier could not see it at all -- such a Reference +// passed with zero violations. This fault is how that hook is shown to have teeth, on the +// same footing as nograce and earlyfree. +// +// Note the obvious-looking fault is the wrong one: clearing MORE references than liveness +// warrants only produces extra nulls, which are safe, and an attempt at it reported +// violations=0 for exactly that reason. The dangling direction is clearing LESS. +int cn1GcFaultRefClear = 0; void cn1GcFaultInitPublic(void); static void cn1GcFaultInit(void) { static int done = 0; @@ -117,6 +128,9 @@ static void cn1GcFaultInit(void) { } else if(strcmp(f, "earlyfree") == 0) { cn1GcFaultEarlyFree = 1; fprintf(stderr, "[GC-FAULT] O(1) page reclaim restored to the pre-fix bound\n"); + } else if(strcmp(f, "refnoclear") == 0) { + cn1GcFaultRefClear = 1; + fprintf(stderr, "[GC-FAULT] dead referents left in place instead of cleared\n"); } else { fprintf(stderr, "[GC-FAULT] unknown fault '%s'\n", f); } @@ -1690,6 +1704,10 @@ static void cn1DrainDeadThreadPending() { static long gcSatbTop = 0; // guarded by gcSatbMutex static long gcSatbCap = 0; static pthread_mutex_t gcSatbMutex = PTHREAD_MUTEX_INITIALIZER; +// Entries cn1SatbEnqueue could not record because its stack would not grow. Read by the +// reference pass, which cannot act on a snapshot that may be missing a referent a mutator +// has already been handed. +_Atomic long cn1SatbDrops = 0; // Monotonic count of objects transitioned unmarked->marked this process; the SATB // drain snapshots it around a batch to detect "marked nothing new" (fixpoint). long gcMarkNewObjectCount = 0; @@ -2061,7 +2079,16 @@ void cn1SatbEnqueue(JAVA_OBJECT old) { if(gcSatbTop >= gcSatbCap) { long ncap = gcSatbCap ? gcSatbCap * 2 : 8192; JAVA_OBJECT* n = (JAVA_OBJECT*)realloc(gcSatbStack, (size_t)ncap * sizeof(JAVA_OBJECT)); - if(n == 0) { pthread_mutex_unlock(&gcSatbMutex); return; } // OOM: drop (rare; only re-opens the original race) + if(n == 0) { + // OOM: drop. For an ordinary STORE that only re-opens the original race and is + // survivable. For a weak REFERENT taken out by Reference.get() it is not -- + // the clear pass would decide on stale liveness and the sweep would free an + // object a mutator is holding -- so record that it happened; the reference + // pass reads this and declines to clear anything this cycle. + atomic_fetch_add_explicit(&cn1SatbDrops, 1, memory_order_relaxed); + pthread_mutex_unlock(&gcSatbMutex); + return; + } gcSatbStack = n; gcSatbCap = ncap; } gcSatbStack[gcSatbTop++] = old; @@ -2076,14 +2103,39 @@ static long cn1SatbTake(JAVA_OBJECT** out) { static JAVA_OBJECT* scratch = 0; static long scratchCap = 0; if(n > scratchCap) { long nc = n < 8192 ? 8192 : n; - scratch = (JAVA_OBJECT*)realloc(scratch, (size_t)nc * sizeof(JAVA_OBJECT)); - scratchCap = nc; + // Through a TEMPORARY. Assigning realloc's result straight back loses the existing + // buffer on failure, and advancing scratchCap alongside it made that permanent: + // every later take saw n <= scratchCap, skipped the realloc, found scratch NULL and + // returned 0, so the barrier kept logging into a stack nothing ever drained again. + JAVA_OBJECT* grown = (JAVA_OBJECT*)realloc(scratch, (size_t)nc * sizeof(JAVA_OBJECT)); + if(grown != 0) { + scratch = grown; + scratchCap = nc; + } + } + // CAPACITY, NOT JUST NON-NULLNESS. Growing through a temporary keeps the old buffer on + // failure, which is what the leak fix wanted -- and it means a FAILED growth leaves + // scratch non-null but SMALLER than n. Testing only scratch != 0 then memcpy'd n + // entries into an allocation sized for fewer: a heap overflow written by the collector + // under memory pressure, which is a far worse failure than the leak it replaced. The + // buffer is usable only if it exists AND is big enough. + JAVA_BOOLEAN usable = (scratch != 0 && scratchCap >= n) ? JAVA_TRUE : JAVA_FALSE; + if(n > 0 && usable) { + memcpy(scratch, gcSatbStack, (size_t)n * sizeof(JAVA_OBJECT)); + } + // TAKE-SIDE LOSS COUNTS AS A DROP TOO. gcSatbTop is reset either way, so entries that + // were successfully logged are discarded here when the scratch buffer could not be + // grown -- and the caller is told the batch was empty, which reads as "termination can + // finish". For a referent Reference.get() has handed out that is the same hazard as a + // failed enqueue and has to invalidate reference clearing the same way; a lost enqueue + // and a lost batch are indistinguishable to the object that gets swept. + if(n > 0 && !usable) { + atomic_fetch_add_explicit(&cn1SatbDrops, 1, memory_order_relaxed); } - if(n > 0 && scratch != 0) memcpy(scratch, gcSatbStack, (size_t)n * sizeof(JAVA_OBJECT)); gcSatbTop = 0; pthread_mutex_unlock(&gcSatbMutex); *out = scratch; - return (scratch != 0) ? n : 0; + return usable ? n : 0; } void cn1RefreshFreeMemCache(void); // defined near cn1BibopMaybeGc; drives the dynamic pacing cap @@ -2166,8 +2218,1068 @@ JAVA_LONG GcVerifyApp_gcMarkState___R_long(CODENAME_ONE_THREAD_STATE) { } #endif +// ---- java.lang.ref: discovery, ranking and clearing ------------------------- +// +// The generated mark function for java.lang.ref.Reference does NOT trace its +// referent (see ByteCodeClass.isReferenceReferent); it calls +// cn1GcDiscoverReference below instead. That is the whole reason a WeakReference +// here is weak: until this existed the referent was an ordinary field, traced +// like any other, so a "weak" reference pinned its referent for the life of the +// process and every cache built on Display.createSoftWeakRef was unbounded. +// +// THE ORDER OF EVENTS IN A CYCLE, because the safety argument is entirely about +// order and every step below depends on the one before it: +// +// 1. cn1RefBeginCycle - empties last cycle's list, recomputes the soft budget +// 2. cn1GcDiscoverReference - called from mark functions as the mark reaches each +// reference. AGES it, decides soft retention, and for a +// retained soft marks the referent so it is an ordinary +// strong edge for the rest of the cycle. +// 3. cn1GcProcessReferences - runs inside the SATB termination loop, after the strong +// mark has reached its fixpoint and while the barrier is +// STILL ARMED. Clears every reference whose referent the +// sweep is about to free. +// +// Step 2 is where ranking costs the mark nothing beyond the walk it was already +// doing. Deciding retention in step 3 instead -- which is what a textbook +// soft-reference implementation does, and what HotSpot does -- would mean computing +// a SECOND reachability closure over the retained set, on a mark that already +// spends most of its time in the grace pass. The price of deciding early is that +// the decision uses the age as of the previous cycle. That is self-correcting: a +// soft reference demoted at the start of a cycle and then read during it is caught +// by the touch test in step 3 and comes back hot on the next one. +// +// WHY STEP 3 MUST RUN WITH THE BARRIER ARMED. This collector scans a thread's stack +// and RELEASES the thread before the others are scanned. A thread released early can +// call get() and park the referent in a local the collector has already walked past +// -- and that referent is by then neither marked nor fresh, which is the one case +// the "a reference stored after the fixpoint is already marked or FRESH" invariant +// the sweep relies on does not cover. So get() carries a SATB load barrier (emitted +// into get_field_java_lang_ref_Reference_objReference), and this pass sits INSIDE the +// termination loop: an enqueue from a racing get() makes the trial clear of +// gcSatbActive find a non-empty log, which re-arms the barrier, marks the referent +// and runs the whole fixpoint again. A reference cleared in the losing half of that +// race is cleared while its referent was genuinely unreachable, which the contract +// permits -- and the referent itself survives, because the enqueue marked it. +// +// TWO PROPERTIES THAT FOLLOW, both legal ("may be cleared" is not "must be cleared"), +// and both worth knowing before writing a test that asserts prompt collection: +// - The sweep's grace rule keeps any object allocated since the last sweep, so a +// referent is never cleared in the cycle it dies. +// - Native C stacks are scanned conservatively, so a stale machine word that looks +// like the referent keeps it marked and get() keeps answering it. + +// Ablation arms for the retention policy. The ranked default is arm 2; the other two +// are the baselines it has to beat, kept compilable so the comparison can be re-run +// rather than remembered. See vm/benchmarks/ab-refs.sh. +// 0 - pressure only: soft referents are retained until headroom drops into the +// pacing reserve, then all of them are dropped at once. This is what the iOS +// port's didReceiveMemoryWarning -> flushSoftRefMap did, moved into the +// collector and without the shared-table lifetime bug. +// 1 - never: a soft reference is as strong as a field. The upper bound on hit rate +// and the upper bound on footprint. +// 2 - ranked by use (default). +// WITHOUT THE SATB BARRIER THERE IS NO SAFE WAY TO CLEAR, so this configuration does not. +// +// -DCN1_DISABLE_SATB is the documented escape hatch for A/B-ing the barrier's cost or +// falling back if it regresses. It compiles out the load barrier that makes handing a weak +// referent to a mutator safe -- but the clear pass is not part of SATB and would keep +// running, so a thread released after its stack scan could load a referent, have the field +// cleared underneath it and the object swept before it could use the pointer. Reference +// processing therefore turns itself off with the barrier, degrading to the behaviour that +// preceded this feature: referents traced strongly and never cleared. That is the right +// fallback for an escape hatch, and it keeps the arm measuring barrier cost rather than +// measuring a different collector. +#if defined(CN1_DISABLE_SATB) && !defined(CN1_NO_WEAK_REFS) +#define CN1_NO_WEAK_REFS 1 +#endif + +#ifndef CN1_REF_POLICY +#define CN1_REF_POLICY 2 +#endif + +// How many collections a soft referent may go untouched and still be kept, when there +// is memory to spare. A budget in CYCLES rather than milliseconds because the +// collector is the only clock that matters here: it is what ages the reference, and a +// busy app collects more often, which is exactly when a cache should be trimmed harder. +#ifndef CN1_REF_SOFT_RETAIN_MAX +#define CN1_REF_SOFT_RETAIN_MAX 8 +#endif + +struct CN1RefEntry { + JAVA_OBJECT ref; // the reference object itself; diagnostics only + JAVA_OBJECT* referentField; + JAVA_INT* touchAgeField; + JAVA_INT strength; + // What this entry's field held when the clear pass nulled it, so a drop that only + // becomes visible AFTER the store can still keep the object alive. Clearing is a + // destructive publish and cannot be taken back; marking the old value can. + JAVA_OBJECT clearedReferent; + // WHICH PASS cleared it, which is what makes clearedReferent recoverable without being + // permanently retained. cn1GcRecoverAfterDrop owes a recovery mark to clears made by an + // EARLIER pass -- those predate the drop that brought it here -- and owes nothing to the + // ones it has just decided itself, after its own quiesce. Without this the two are + // indistinguishable, so it marked every referent it had condemned one statement earlier + // and reclaimed nothing, which is the blanket retention it exists to avoid. + // + // A drop count cannot answer this. The getter this record defends against fails its + // enqueue BEFORE the clear stamps anything, so the count at the clear already includes + // it and "the count moved since" is false exactly when the recovery is owed. Pass + // identity is exact, and the drop count is then used once, for the whole pass, after a + // quiesce that bounds the window. + long clearedAtPass; +}; +static struct CN1RefEntry* cn1RefDiscovered = 0; +static long cn1RefDiscoveredTop = 0; +static long cn1RefDiscoveredCap = 0; +// Discovery runs from generated mark functions, which gcMarkDrainParallel may fan out +// across a worker pool, so the append is locked. It is a cold lock by construction: +// it is taken once per REFERENCE per cycle, where the SATB mutex next door is taken +// per logged store. +static pthread_mutex_t cn1RefMutex = PTHREAD_MUTEX_INITIALIZER; +// Ages, in collections, that a soft referent may reach before it is dropped. +// Recomputed once per cycle by cn1RefBeginCycle. Negative means "drop every soft +// referent, however hot". +// +// ATOMIC because the writer and the readers are different threads: cn1RefBeginCycle +// runs on the GC thread and cn1GcDiscoverReference reads it from however many mark +// workers gcMarkDrainParallel is using. Relaxed is the same instruction on every target +// built here; what it buys is that the pair is not a mixed atomic/non-atomic access, +// which is undefined in C however benign the race looks. Reading a value one cycle stale +// would be harmless anyway -- the budget only decides retention, never safety. +static _Atomic int cn1SoftRetainCycles = CN1_REF_SOFT_RETAIN_MAX; + +// Raised when an allocation has actually FAILED, and consumed by the next cycle. +// +// The retention ladder is driven by cn1ProcessHeadroom(), which answers -1 wherever there +// is no per-process budget to probe -- every desktop build, and the simulator. There the +// ladder can only ever say "plenty", so a soft referent read at least once every +// CN1_REF_SOFT_RETAIN_MAX cycles is never dropped however tight memory actually is. That +// is a hole rather than a conservative default, because the one guarantee SoftReference +// makes is precisely about this case: every soft reference is cleared before the VM gives +// up. codenameOneGcMalloc's failure path only asks for a collection and retries, so a heap +// held entirely through live soft references would have turned recoverable pressure into an +// allocation loop that collects nothing. +// +// A LATCH rather than a direct write of the budget, because cn1RefBeginCycle recomputes +// that budget from scratch at the top of every cycle and would erase a direct write before +// a single reference was reached. +static _Atomic int cn1RefEmergencyDrop = 0; + +// currentGcMarkValue of the cycle whose clear pass has already aged the discovered set. +// GC thread only. The pass can run more than once in a cycle -- every SATB reopen runs it +// again -- and ageing on each of those would count one collection as several and evict a +// cache by however many times the mutator happened to storm the reference log. +static int cn1RefAgedCycle = 0; + +// cn1SatbDrops as it stood when this cycle began. If it has moved by the time the clear +// pass runs, some referent handed to a mutator never reached the log and the pass has no +// sound basis for clearing anything. +static long cn1RefDropsAtCycleStart = 0; +// Identifies one run of the reference machinery, so an entry can say which pass cleared it. +// Bumped once per cn1GcProcessReferences, which is also the only caller of +// cn1GcRecoverAfterDrop -- the two never clear within the same pass, because the recovery +// path returns before the main loop. Read on the collector only. +static long cn1RefPass = 0; + +// Referents cleared by the EMERGENCY path, which runs at discovery and has no list entry +// to remember them in -- that path exists precisely because the list could not grow. +// Fixed and preallocated, because allocating here is what is unavailable: the emergency is +// raised by an allocation failure. +// +// Overflow is handled by not clearing at all: past this many the emergency marks the +// referent instead, which retains memory it wanted to release but never hands out a +// dangling pointer. 512 is far above the number of references that can reach this path in +// practice, since it needs the discovery list to have failed to grow first. +#define CN1_REF_EMERGENCY_SLOTS 512 +static JAVA_OBJECT cn1RefEmergencyCleared[CN1_REF_EMERGENCY_SLOTS]; +// ATOMIC, because this is reached with cn1RefMutex already released and mark functions run +// on however many workers gcMarkDrainParallel is using. A plain post-increment there loses +// increments and lets two workers publish into one slot, so a referent that WAS cleared +// goes unrecorded -- and an unrecorded emergency clear is exactly the case the drop +// recovery cannot repair, leaving the sweep free to take it under a racing get(). +static _Atomic long cn1RefEmergencyTop = 0; + +// Called from the allocation-failure path. Idempotent, allocation-free and safe from a +// thread that is about to park -- a relaxed store and nothing else. +void cn1RefDropAllSoftReferents(void) { + atomic_store_explicit(&cn1RefEmergencyDrop, 1, memory_order_relaxed); +} + +#ifdef CN1_GC_CONFORM +_Atomic long cn1RefDiscoveries = 0; // references the mark reached (deduped) +_Atomic long cn1RefWeak = 0; // of those, weak +_Atomic long cn1RefRetained = 0; // soft referents kept by the policy +_Atomic long cn1RefKeptTouched = 0; // kept by the racing-get() rule in the clear pass +_Atomic long cn1RefCleared = 0; // referents handed to the sweep +_Atomic long cn1RefGets = 0; // Reference.get() calls, for costing the load barrier +long long cn1RefPhaseNs = 0; // GC thread only: time in cn1GcProcessReferences +long cn1RefPasses = 0; // clear passes run this cycle (>1 == SATB reopen) +#endif + +// Claim a recovery slot and publish the referent into it, or report that there is none. +// Reserving BEFORE the field is cleared is the point: a slot that could not be claimed +// means the clear must not happen, because nothing would be able to undo it. +static JAVA_BOOLEAN cn1RefEmergencyReserve(JAVA_OBJECT r) { + long idx = atomic_fetch_add_explicit(&cn1RefEmergencyTop, 1, memory_order_relaxed); + if(idx >= CN1_REF_EMERGENCY_SLOTS) { + return JAVA_FALSE; // full: caller marks instead of clearing + } + cn1RefEmergencyCleared[idx] = r; + return JAVA_TRUE; +} + +// Mark every referent the emergency path cleared, for the recoveries below. +static JAVA_BOOLEAN cn1RefRecoverEmergency(CODENAME_ONE_THREAD_STATE) { + JAVA_BOOLEAN any = JAVA_FALSE; + long top = atomic_load_explicit(&cn1RefEmergencyTop, memory_order_relaxed); + if(top > CN1_REF_EMERGENCY_SLOTS) { + top = CN1_REF_EMERGENCY_SLOTS; + } + for(long i = 0 ; i < top ; i++) { + JAVA_OBJECT was = cn1RefEmergencyCleared[i]; + if(was != JAVA_NULL && !CN1_IS_TAGGED(was)) { + gcMarkObject(threadStateData, was, JAVA_FALSE); + any = JAVA_TRUE; + } + } + return any; +} + +// Mark every referent the collector knows about -- discovered, and emergency-cleared -- +// until a drain stops finding more. The safe direction whenever clearing cannot be +// justified: it costs a cycle's reclaim and can never hand out a freed pointer. +static JAVA_BOOLEAN cn1GcRetainAllReferences(CODENAME_ONE_THREAD_STATE) { + JAVA_BOOLEAN marked = JAVA_FALSE; + for(;;) { + // BOTH COUNTERS, because discovery has two ways to make progress. The drain can + // reach a reference whose referent the emergency path clears on the spot, and that + // path deliberately does NOT append to the discovery list -- it exists because the + // list could not grow. Watching only cn1RefDiscoveredTop therefore reads "nothing + // new" while a fresh recovery slot has just been filled, and the loop leaves + // without marking the referent it holds. + long before = cn1RefDiscoveredTop; + long beforeEmergency = atomic_load_explicit(&cn1RefEmergencyTop, memory_order_relaxed); + JAVA_BOOLEAN round = JAVA_FALSE; + for(long i = 0 ; i < before ; i++) { + JAVA_OBJECT r = __atomic_load_n(cn1RefDiscovered[i].referentField, __ATOMIC_RELAXED); + if(r != JAVA_NULL && !CN1_IS_TAGGED(r)) { + gcMarkObject(threadStateData, r, JAVA_FALSE); + round = JAVA_TRUE; + } + JAVA_OBJECT was = cn1RefDiscovered[i].clearedReferent; + if(was != JAVA_NULL && !CN1_IS_TAGGED(was)) { + gcMarkObject(threadStateData, was, JAVA_FALSE); + round = JAVA_TRUE; + } + } + if(cn1RefRecoverEmergency(threadStateData)) { + round = JAVA_TRUE; + } + if(round) { + marked = JAVA_TRUE; + gcMarkDrain(threadStateData); + } + if(cn1RefDiscoveredTop == before + && atomic_load_explicit(&cn1RefEmergencyTop, memory_order_relaxed) == beforeEmergency) { + return marked; + } + } +} + +// Recovery after an SATB loss, which must keep racing loads alive WITHOUT undoing the +// emergency's reclaim. +// +// Retaining everything is the safe reflex and it deadlocks the allocator: the emergency +// budget is raised by an allocation failure, sustained exhaustion keeps the SATB stack from +// growing, and if soft-referenced data is what exhausted memory then every retry cycle +// loses a batch, retains the same data, and codenameOneGcMalloc spins forever on a +// collection that frees nothing. +// +// The emergency decision does not depend on the log. "Drop every soft referent" is a policy +// choice made from the memory budget at cycle start, not an inference from liveness, so a +// lost log entry does not invalidate it. What the log WOULD have protected is a referent a +// mutator is mid-read of -- and the touch stamp records that independently, allocation-free, +// which is the same signal sub-pass A relies on. +// +// So: touched referents are retained, condemned soft referents are still cleared, and +// everything else is retained. The residual is a get() that loaded a soft referent and was +// descheduled before stamping; that window is the one the emergency path already accepts +// and documents, and the alternative to accepting it is an allocator that cannot progress. +static JAVA_BOOLEAN cn1GcRecoverAfterDrop(CODENAME_ONE_THREAD_STATE) { + int budget = atomic_load_explicit(&cn1SoftRetainCycles, memory_order_relaxed); + if(budget >= 0) { + return cn1GcRetainAllReferences(threadStateData); // no emergency: retain freely + } + JAVA_BOOLEAN marked = JAVA_FALSE; + // QUIESCE BEFORE READING ANY TOUCH STAMP. The drop counter becomes visible when an + // enqueue fails, which is BEFORE the accessor reaches its stamp -- so entering here on + // that signal and reading touchAgeField immediately can see "not touched" for a getter + // that is mid-load and about to stamp. Clearing on that reading hands the sweep an + // object the getter is being given, and this path deliberately does not record + // clearedReferent, so nothing downstream could recover it. + // + // Every getter registers across its whole load, stamp included, so an in-flight count + // of zero means every getter that overlapped has finished and published. That is what + // makes the stamp readable as evidence rather than as a race. + cn1SatbBulkQuiesce(); + // READ AFTER THE QUIESCE, DELIBERATELY. Every getter in flight when this pass began has + // now finished and published, so a drop counted from here belongs to one that started + // afterwards -- the only kind that can still be holding a referent this pass is about + // to condemn. Taken before the quiesce it would also count drops the quiesce has + // already accounted for, and the post-loop check would retain on every one of them. + long dropsAfterQuiesce = atomic_load_explicit(&cn1SatbDrops, memory_order_relaxed); + for(;;) { + long before = cn1RefDiscoveredTop; + long beforeEmergency = atomic_load_explicit(&cn1RefEmergencyTop, memory_order_relaxed); + JAVA_BOOLEAN round = JAVA_FALSE; + for(long i = 0 ; i < before ; i++) { + struct CN1RefEntry* e = &cn1RefDiscovered[i]; + JAVA_OBJECT r = __atomic_load_n(e->referentField, __ATOMIC_RELAXED); + if(r != JAVA_NULL && !CN1_IS_TAGGED(r)) { + // RESOLVE BEFORE READING THE HEADER. A Reference kept alive by a stale + // native-stack word can hold a referent swept in an earlier cycle whose + // memory is unmapped, and reading even its mark word faults. The clear + // pass and the unrecorded emergency path both guard this; the guard was + // omitted here when this mark-word test was added one commit ago. + JAVA_BOOLEAN usable = JAVA_TRUE; +#ifdef CN1_CONSERVATIVE_GC_ROOTS + if(cn1ConservativeResolve((void*)r) != r && !cn1GcImmortalObjContains(r)) { + usable = JAVA_FALSE; + } +#endif + // A referent already marked, or fresh, is reachable some other way -- a + // strong edge or a root -- and a SoftReference may only be cleared when + // its referent is SOFTLY reachable. Omitting this would clear an + // application's cache entry for an object it also holds in a field. + JAVA_BOOLEAN reachableOtherwise = JAVA_TRUE; + if(usable) { + int rm = __atomic_load_n(&r->__codenameOneGcMark, __ATOMIC_ACQUIRE); + reachableOtherwise = + (rm == currentGcMarkValue || rm == -1) ? JAVA_TRUE : JAVA_FALSE; + } + if(usable + && !reachableOtherwise + && e->strength == CN1_REF_SOFT + && __atomic_load_n(e->touchAgeField, __ATOMIC_RELAXED) != CN1_REF_TOUCHED) { + // RECORDED, even though recording can cost the reclaim. + // + // The quiesce above drains the getters that were in flight when this + // began; it cannot stop a new one registering immediately after, and + // that getter can load r, have its enqueue fail, and be descheduled + // before stamping -- so this loop sees the old stamp and clears a field + // whose referent is being handed out. Without a record nothing later + // could mark it and the sweep would take it. + // + // Saving it means the post-clear recovery marks it when a drop is + // visible, which yields the emergency's reclaim back in exactly the + // situation where safety is uncertain -- the right way round. In the + // common case, where no further drop occurs, the clear stands and the + // reclaim happens. + e->clearedReferent = r; + e->clearedAtPass = cn1RefPass; + __atomic_store_n(e->referentField, JAVA_NULL, __ATOMIC_RELAXED); +#ifdef CN1_GC_CONFORM + atomic_fetch_add_explicit(&cn1RefCleared, 1, memory_order_relaxed); +#endif + } else if(usable) { + gcMarkObject(threadStateData, r, JAVA_FALSE); + round = JAVA_TRUE; + } + } + // RECOVER ONLY WHAT A LATER DROP CALLED INTO QUESTION. This block used to + // mark every clearedReferent unconditionally, and the clear above records one + // -- so an entry condemned by this very loop was marked one statement later, + // its object survived the sweep, and the emergency reclaimed nothing. That is + // the blanket retention this function exists to avoid: the caller reaches it + // precisely when soft-referenced data is what is exhausting memory, and + // retaining it there is what lets the allocator spin. + // + // The clear is still recoverable, which is the point of recording it. A drop + // that becomes visible after the store means a getter may have taken the + // referent without the collector seeing the keep-alive, and the comparison + // below is what tells that apart from the drop that brought us here -- which + // the clear decision above already accounted for, after its own quiesce. + JAVA_OBJECT was = e->clearedReferent; + if(was != JAVA_NULL && !CN1_IS_TAGGED(was) && e->clearedAtPass != cn1RefPass) { + gcMarkObject(threadStateData, was, JAVA_FALSE); + round = JAVA_TRUE; + } + } + if(cn1RefRecoverEmergency(threadStateData)) { + round = JAVA_TRUE; + } + if(round) { + marked = JAVA_TRUE; + gcMarkDrain(threadStateData); + } + if(cn1RefDiscoveredTop == before + && atomic_load_explicit(&cn1RefEmergencyTop, memory_order_relaxed) == beforeEmergency) { + break; + } + } + // THE SAME RE-CHECK THE MAIN PASS MAKES, and for the same reason. The fixpoint above + // exits on the discovery counters, which a drop does not move, so without this a drop + // raised while this pass was clearing would be recovered only if some LATER call + // happened to run -- and the final cn1GcProcessReferences of a cycle has none after + // it. The clears stand either way; what this restores is the mark, so the sweep cannot + // free an object a getter took while the log was dropping. + cn1SatbBulkQuiesce(); + if(atomic_load_explicit(&cn1SatbDrops, memory_order_relaxed) != dropsAfterQuiesce) { + if(cn1GcRetainAllReferences(threadStateData)) { + marked = JAVA_TRUE; + } + } + return marked; +} + +// Recompute the soft budget and drop the previous cycle's discoveries. Called from +// codenameOneGCMark before anything can mark. +static void cn1RefBeginCycle(void) { + cn1RefDiscoveredTop = 0; + cn1RefDropsAtCycleStart = atomic_load_explicit(&cn1SatbDrops, memory_order_relaxed); + atomic_store_explicit(&cn1RefEmergencyTop, 0, memory_order_relaxed); +#ifdef CN1_GC_CONFORM + // PER CYCLE, like every other figure in [GCPROBE]. A running total cannot show + // whether the budget is tracking memory pressure, which is the whole question the + // ranking has to answer. + atomic_store_explicit(&cn1RefDiscoveries, 0, memory_order_relaxed); + atomic_store_explicit(&cn1RefWeak, 0, memory_order_relaxed); + atomic_store_explicit(&cn1RefRetained, 0, memory_order_relaxed); + atomic_store_explicit(&cn1RefKeptTouched, 0, memory_order_relaxed); + atomic_store_explicit(&cn1RefCleared, 0, memory_order_relaxed); + // cn1RefGets is NOT reset: it is a whole-run total, because the question it answers + // ("how often does a real workload call get()?") is about the run, not the cycle. + cn1RefPhaseNs = 0; + cn1RefPasses = 0; +#endif + // THE EMERGENCY LATCH OUTRANKS EVERY POLICY, including "never clear" -- an arm that + // exists to bound the measurement, not to promise a soft reference outlives an + // out-of-memory. Exchange rather than load-then-clear so a failure raised while this + // runs is either honoured now or survives to the next cycle, never dropped between. + if(atomic_exchange_explicit(&cn1RefEmergencyDrop, 0, memory_order_relaxed)) { + atomic_store_explicit(&cn1SoftRetainCycles, -1, memory_order_relaxed); + return; + } +#if CN1_REF_POLICY == 1 + atomic_store_explicit(&cn1SoftRetainCycles, 0x7fffffff, memory_order_relaxed); +#else + { + long headroom = cn1ProcessHeadroom(); + if(headroom < 0) { + // No per-process limit on this platform -- desktop, CI, the simulator. Age + // soft referents out at the full budget anyway rather than keeping them + // forever: an unbounded cache is the defect this replaces, and a host with + // plenty of RAM is precisely where it went unnoticed for years. + atomic_store_explicit(&cn1SoftRetainCycles, CN1_REF_SOFT_RETAIN_MAX, + memory_order_relaxed); + } else { + // Against the BUDGET, never the device's free RAM, and the budget is what is + // already spent plus what is left. Sizing this from the device is the defect + // #5563 fixed for the pacing cap, and it is the same defect whichever + // consumer reads the number. + long long footprint = (long long)cn1ProcFootprintBytes(); + long long budget = (footprint > 0 ? footprint : 0) + (long long)headroom; + // + // BANDS AS EXPLICIT FRACTIONS OF THE BUDGET. An earlier version wrote them as + // multiples of the pacing reserve (budget >> 2) and got both ends wrong in a + // way that reads as correct: `reserve * 4` IS the whole budget, so the top band + // required headroom to equal the budget and could never be entered, while the + // bottom band required headroom below a quarter -- which the pacing loop + // actively prevents, since defending that reserve is its job. Written as + // fractions the reachability of each band is obvious on inspection. +#if CN1_REF_POLICY == 0 + // Pressure only, no ranking: everything, until nothing. This is what the iOS + // port's didReceiveMemoryWarning -> flushSoftRefMap does, and it is the arm + // ranking has to beat. It switches at the SAME point the ranked ladder starts + // trimming, so the two differ in one thing only -- what they drop once they + // have decided to drop something. + atomic_store_explicit(&cn1SoftRetainCycles, + (headroom * 2 >= budget) ? 0x7fffffff : -1, + memory_order_relaxed); +#else + if(headroom <= 0) { + atomic_store_explicit(&cn1SoftRetainCycles, -1, memory_order_relaxed); + } else if(headroom * 2 >= budget) { + // Over half the budget still free: nothing to trim for. + atomic_store_explicit(&cn1SoftRetainCycles, CN1_REF_SOFT_RETAIN_MAX, + memory_order_relaxed); + } else if(headroom * 4 >= budget) { + atomic_store_explicit(&cn1SoftRetainCycles, CN1_REF_SOFT_RETAIN_MAX / 2, + memory_order_relaxed); + } else { + // Inside the quarter the pacing loop defends: keep only what was read + // since the last collection. + atomic_store_explicit(&cn1SoftRetainCycles, 0, memory_order_relaxed); + } +#endif + } + } +#endif +} + +void cn1GcDiscoverReference(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT ref, JAVA_BOOLEAN force, + JAVA_OBJECT* referentField, JAVA_INT* touchAgeField, + JAVA_INT* agedCycleField, JAVA_INT strength) { + // RELAXED ATOMIC on referentField and touchAgeField everywhere they are touched. + // Both are written by the generated accessors on mutator threads while this runs on + // the collector, which is the concurrency this design exists to support -- so a plain + // access on either side is a data race and undefined in C, however benign the emitted + // instruction is on the targets built here. Same treatment the mark word already gets. + if(referentField == 0 + || __atomic_load_n(referentField, __ATOMIC_RELAXED) == JAVA_NULL) { + return; // already cleared: nothing to decide + } +#ifdef CN1_NURSERY + // A NURSERY PROMOTION IS NOT A WEAK-REFERENCE DECISION. cn1PromoteDrain runs the + // generated mark functions with nurseryPromoting raised, and gcMarkObject's promotion + // branch is what moves a referenced object out of the block being recycled. Routing the + // referent here instead skips that branch entirely: a surviving WeakReference whose + // referent sits in a different nursery block would be promoted alone, the minor + // collector would recycle the referent's block, and the field -- never cleared, because + // this is not a collection cycle -- would be left pointing into it. + // + // During promotion the referent is therefore treated exactly like any other field. No + // clearing happens on this path in any case, so making the edge strong here costs a + // promotion and nothing else. + if(threadStateData != 0 && threadStateData->nurseryPromoting) { + gcMarkObject(threadStateData, __atomic_load_n(referentField, __ATOMIC_RELAXED), force); + return; + } +#endif +#ifdef CN1_GC_VERIFY + // THE VERIFIER HAS TO SEE THE REFERENT, and it could not. + // + // cn1GcVerifyHeap walks the surviving objects through the SAME generated mark + // functions, relying on every reference field arriving at gcMarkObject, whose verify + // branch classifies it. Suppressing that call for the referent -- which is what makes + // the edge weak -- also took the referent out of the verifier's reach, so a live + // Reference holding a pointer into reclaimed memory passed with zero violations. That + // is precisely the defect this collector work most needs the verifier to catch, and + // every "clean over N verify passes" result recorded on this branch before this hook + // existed was silent about the referent specifically. + // + // Answered here, ahead of the ageing and the dedupe: a verify walk is not a collection + // cycle, and letting it age references or mark cn1AgedCycle would corrupt the state the + // next real cycle reads -- and the dedupe would drop the second and later visits, which + // are exactly the ones a walk of the whole heap produces. + if(cn1GcVerifyActive) { + cn1GcVerifyChild(__atomic_load_n(referentField, __ATOMIC_RELAXED), + __builtin_return_address(0)); + return; + } +#endif +#ifdef CN1_NO_WEAK_REFS + // ABLATION ARM: trace the referent strongly and never clear anything, which is + // exactly what this VM did before references were implemented. It exists so the + // gate can prove it is not inert -- a test that asserts referents get collected + // must FAIL when built this way, or it is asserting something else. + gcMarkObject(threadStateData, __atomic_load_n(referentField, __ATOMIC_RELAXED), force); + (void)touchAgeField; (void)agedCycleField; (void)strength; (void)ref; + return; +#else + JAVA_BOOLEAN retain; + pthread_mutex_lock(&cn1RefMutex); + // DEDUPE, and it is not an optimization. Being reached more than once in a cycle is + // the normal case, not a rare one: force-marking re-runs mark functions over + // already-marked objects once per statics pass and again for the constant pool. Left + // undeduped, a popular reference would age several times a cycle and its soft + // referent would be dropped that many times sooner. + if(*agedCycleField == currentGcMarkValue) { + pthread_mutex_unlock(&cn1RefMutex); + return; + } + *agedCycleField = currentGcMarkValue; + { + // READ the age; do NOT consume it. The ageing write lives at the end of the clear + // pass instead, and that placement is a safety property rather than tidiness. + // + // Consuming CN1_REF_TOUCHED here would erase every get() that happened between the + // start of the cycle and this moment, so the clear pass's "was it read?" fallback + // would only ever have covered reads that landed AFTER discovery. It would also + // lose the touch outright under a race -- read 5, a mutator stamps TOUCHED, write 6 + // -- leaving a just-used referent looking cold with no record that it was used at + // all. Deferring the write means any read anywhere in the cycle is still visible to + // sub-pass A. + // + // CN1_REF_TOUCHED is negative, so it compares as hotter than any real age and a + // reference read this cycle is retained by the test below without a special case. + JAVA_INT age = __atomic_load_n(touchAgeField, __ATOMIC_RELAXED); + int budget = atomic_load_explicit(&cn1SoftRetainCycles, memory_order_relaxed); + retain = (strength == CN1_REF_SOFT && budget >= 0 && age <= budget) + ? JAVA_TRUE : JAVA_FALSE; + } + if(cn1RefDiscoveredTop >= cn1RefDiscoveredCap + && atomic_load_explicit(&cn1GcFreezeHeld, memory_order_relaxed) == 0) { + // NEVER GROW WHILE A THREAD IS SIGNAL-FROZEN -- the same rule cn1MatureObject's + // adoption buffer follows, and the reason cn1GcFreezeHeld exists. A frozen thread + // halts at an arbitrary instruction and can own the libc allocator lock, so a + // realloc here would block the collector until a thread that only resumes when the + // collector lets it. Declining costs a deferred reclaim (see below) and nothing else. + // + // This is belt and braces rather than a live hazard: discovery runs from mark + // functions, which run in the drain, which runs after every freeze is released. + // Belt and braces is right for a rule whose violation is a whole-process hang that + // reproduces on one thread in one interleaving. + long nc = cn1RefDiscoveredCap == 0 ? 256 : cn1RefDiscoveredCap * 2; + struct CN1RefEntry* grown = + (struct CN1RefEntry*)realloc(cn1RefDiscovered, (size_t)nc * sizeof(struct CN1RefEntry)); + if(grown != 0) { + cn1RefDiscovered = grown; + cn1RefDiscoveredCap = nc; + } + } + JAVA_BOOLEAN recorded = JAVA_FALSE; + if(cn1RefDiscoveredTop < cn1RefDiscoveredCap) { + struct CN1RefEntry* e = &cn1RefDiscovered[cn1RefDiscoveredTop++]; + e->ref = ref; + e->referentField = referentField; + e->touchAgeField = touchAgeField; + e->strength = strength; + e->clearedReferent = JAVA_NULL; + e->clearedAtPass = 0; + recorded = JAVA_TRUE; + } + pthread_mutex_unlock(&cn1RefMutex); + // A DROPPED ENTRY MUST BE RETAINED, NOT IGNORED -- whether it was dropped because the + // realloc failed or because the growth above was declined while a thread is frozen. + // + // An earlier version of this comment claimed the opposite, that dropping was safe + // because "the referent stays reachable through a field nothing cleared". That is + // exactly backwards, and it is worth spelling out because it reads as obviously true: + // not clearing the field is not the same as keeping the referent ALIVE. Nothing else + // marks it -- that is the whole point of a weak edge -- so the sweep frees it, and the + // field nothing cleared is then a dangling pointer inside a perfectly reachable + // Reference, handed to the next get(). On this VM that is a native crash no Java catch + // can see, which is the worst possible outcome for a path taken only when memory is + // already short. + // + // Marking is the conservative direction: the referent survives one more cycle, the + // reference is rediscovered next time, and by then the list has usually grown. + if(!recorded) { + // THE EMERGENCY MUST NOT BE DEFEATED BY THE FALLBACK. Marking unconditionally here + // is right for a weak reference and wrong for a soft one at the exact moment it + // matters: the emergency budget is raised by an allocation FAILURE, and an + // allocation failure is also the most likely reason the list could not grow. So + // the two mechanisms met and the conservative one won -- soft referents past the + // list's capacity were marked and kept, the collection freed nothing, and + // codenameOneGcMalloc's retry loop had nothing to make progress against. + // + // A soft reference the emergency has condemned needs no list: the decision is + // already final, so the field can be cleared here and now, which is exactly the + // allocation-free path this situation calls for. It is safe for the same reason + // the ordinary clear is -- a get() that already loaded the referent enqueued it + // through the armed barrier and keeps it alive for this cycle, and a get() after + // this store reads null. + // + // Everything else still takes the conservative branch: weak references, and + // anything read since the last ageing, where a mutator may be holding the referent + // in a local the collector has walked past. That leaves the possibility of two + // aliases of one soft referent disagreeing when only some were recorded -- accepted + // deliberately, because it is confined to soft references, which are caches by + // definition, and the alternative is an allocator that cannot make progress. The + // lifetime-oracle callers use weak references, which take the marking branch. + JAVA_OBJECT r = __atomic_load_n(referentField, __ATOMIC_RELAXED); + // NOT IF IT IS ALREADY MARKED. A soft reference may only be cleared when its + // referent is softly reachable, and an object already marked at this point is + // reachable some other way -- through an ordinary strong edge, or as a root. An + // application holding both a field and a SoftReference to one object would + // otherwise watch get() answer null under allocation pressure for an object that + // was never a candidate for collection at all. + // + // Partial, and deliberately so: the mark is still in progress here, so a referent + // that a strong edge reaches LATER in this cycle is not yet marked and can still be + // cleared. Being sure would mean deferring to the clear pass, which is precisely + // what this path exists because it cannot do -- the list is full and cannot grow. + // Under genuine exhaustion the residue is a spurious cache miss on an object that + // stays alive, against an allocator that otherwise cannot make progress. + JAVA_BOOLEAN alreadyLive = JAVA_FALSE; + JAVA_BOOLEAN resolvable = (r != JAVA_NULL && !CN1_IS_TAGGED(r)) ? JAVA_TRUE : JAVA_FALSE; +#ifdef CN1_CONSERVATIVE_GC_ROOTS + // RESOLVE BEFORE DEREFERENCING, exactly as the clear pass does. A dead Reference + // kept alive by a stale native-stack word can hold a referent that was swept in an + // earlier cycle and whose memory is now unmapped -- and gcMarkObject's own comment + // says reading even the mark word of such a pointer faults. The clear pass guards + // for this; this fallback read its header first and reached gcMarkObject's + // validation only afterwards, so a collection under memory pressure could take the + // process down. An unresolvable pointer is treated as not-live, which is what it + // is: garbage, or an object allocated after this cycle's extent snapshot and + // therefore kept by the grace rule regardless. + if(resolvable && cn1ConservativeResolve((void*)r) != r && !cn1GcImmortalObjContains(r)) { + resolvable = JAVA_FALSE; + } +#endif + if(resolvable) { + int rm = __atomic_load_n(&r->__codenameOneGcMark, __ATOMIC_ACQUIRE); + alreadyLive = (rm == currentGcMarkValue || rm == -1) ? JAVA_TRUE : JAVA_FALSE; + } + // Only clear if the referent can be REMEMBERED. A racing get() can load it and + // then fail to log it, and this path has no entry the late-drop recovery could + // consult -- so without a record the sweep would free a pointer already handed to a + // mutator. With nowhere to record it, marking is the answer: it keeps memory the + // emergency wanted back, which is a worse outcome than clearing and a far better + // one than a dangling read. + if(!alreadyLive + && resolvable + && strength == CN1_REF_SOFT + && atomic_load_explicit(&cn1SoftRetainCycles, memory_order_relaxed) < 0 + && __atomic_load_n(touchAgeField, __ATOMIC_RELAXED) != CN1_REF_TOUCHED + && cn1RefEmergencyReserve(r)) { + __atomic_store_n(referentField, JAVA_NULL, __ATOMIC_RELAXED); +#ifdef CN1_GC_CONFORM + atomic_fetch_add_explicit(&cn1RefCleared, 1, memory_order_relaxed); +#endif + } else { + gcMarkObject(threadStateData, r, force); + // CONSUME THE TOUCH, having retained the referent for this cycle. + // + // Nothing else will. The ageing loop walks cn1RefDiscovered, and this reference + // is here precisely because it could not be recorded there -- so a stamp left + // at CN1_REF_TOUCHED stays that way for the life of the process. The condition + // above then refuses to clear on every subsequent emergency cycle, the soft + // referent is retained forever however long ago it was last read, and if that + // memory is what is blocking the allocation, codenameOneGcMalloc's retry loop + // never makes progress. That is the livelock this whole emergency path exists + // to break, reached through the one reference it cannot write down. + // + // Safe because the referent was just MARKED: a mutator holding it is covered + // for this cycle, which is all the stamp was protecting. If it is read again + // the next get() stamps it afresh and it is retained again; if it is not, the + // next emergency cycle is free to clear it. Compare-exchange rather than a + // plain store so a get() landing in between is not silently overwritten. + JAVA_INT expected = CN1_REF_TOUCHED; + __atomic_compare_exchange_n(touchAgeField, &expected, 0, 0, + __ATOMIC_RELAXED, __ATOMIC_RELAXED); + } + return; + } +#ifdef CN1_GC_CONFORM + atomic_fetch_add_explicit(&cn1RefDiscoveries, 1, memory_order_relaxed); + if(strength != CN1_REF_SOFT) { + atomic_fetch_add_explicit(&cn1RefWeak, 1, memory_order_relaxed); + } else if(retain) { + atomic_fetch_add_explicit(&cn1RefRetained, 1, memory_order_relaxed); + } +#endif + if(retain) { + // A retained soft reference is an ordinary strong edge for the rest of this + // cycle, traced with whatever `force` the caller had -- a reference held by a + // static is force-marked like anything else it points at. + gcMarkObject(threadStateData, __atomic_load_n(referentField, __ATOMIC_RELAXED), force); + } +#endif +} + +// Clear every discovered reference whose referent this cycle's sweep is about to free. +// Runs on the GC thread only, inside the SATB termination loop, barrier armed. +// Returns JAVA_TRUE if it marked anything; it has already drained by then. +static JAVA_BOOLEAN cn1GcProcessReferences(CODENAME_ONE_THREAD_STATE) { + // NOTHING TO DO IN A PROGRAM WITH NO REFERENCES, and doing nothing has to mean + // touching nothing. Most applications never construct a Reference, and for those this + // pass previously still ran a cn1SatbBulkQuiesce() on every outer termination pass -- + // which is not free: it spins in usleep(50) while any BULK ARRAY COPY is in flight, so + // an allocation-heavy program that arraycopies object arrays pays a collector stall for + // a feature it does not use. + // + // The early exit is also the answer to a CI regression that took a long time to find: + // BibopPageFloorIntegrationTest, whose workload contains no Reference at all, began + // failing intermittently on the 4-marker arm64 configuration once that quiesce landed, + // reporting its pages as released while the footprint stayed put. The base feature + // commit -- weak references with none of this machinery -- passes that job, which is + // what localised it here. + if(cn1RefDiscoveredTop == 0 + && atomic_load_explicit(&cn1RefEmergencyTop, memory_order_relaxed) == 0) { + return JAVA_FALSE; + } + JAVA_BOOLEAN marked = JAVA_FALSE; + // AFTER the early exit, so the identity only advances on a pass that can actually + // clear something. Bumping it above would be harmless but would let the counter run in + // applications that hold no Reference at all, which is the case the exit exists for. + cn1RefPass++; +#ifdef CN1_GC_CONFORM + long long __r0 = cn1GcNowNs(); + cn1RefPasses++; +#endif + // No lock. Discovery only ever appends, and every mark thread has been drained to a + // fixpoint before the caller reaches this, so the prefix walked here is stable. A + // re-run after a SATB reopen walks the list again from the start, which is what picks + // up anything discovered by the re-opened fixpoint. + long n; + +#ifdef CN1_REF_NO_ALIAS_ATOMICITY + // ABLATION ARM: the single-loop form the two sub-passes replaced, which re-read the + // touch stamp per ENTRY and could therefore clear one alias of a referent and keep + // another. + // + // IT HAS NEVER BEEN SEEN TO FAIL, and that is recorded rather than hidden. The split + // needs a get() to land between the pass reaching one alias and reaching another, and + // that window is microseconds; RefPolicy's alias phase with a burst reader reported + // ALIAS_SPLIT=0/256 built THIS way, identical to the fixed build, while both collected + // the same groups. The defect is real by inspection -- the loop plainly re-reads a + // stamp a mutator can change mid-pass -- and the two-sub-pass form removes the + // possibility rather than narrowing the window, which is why it is the shipped one. + // The arm stays so a future attempt at a reproducer has something to aim at, on the + // same footing as CN1_NO_BULK_INSERTION_BARRIER. Not a supported configuration. + n = cn1RefDiscoveredTop; + for(long i = 0 ; i < n ; i++) { + struct CN1RefEntry* e = &cn1RefDiscovered[i]; + JAVA_OBJECT r = __atomic_load_n(e->referentField, __ATOMIC_RELAXED); + if(r == JAVA_NULL || CN1_IS_TAGGED(r)) { + continue; + } + { + // Ages inline, as the single-loop form did. Discovery no longer ages, so + // without this the arm would never age anything and would silently become + // "retain everything" rather than the shape it exists to reproduce. + JAVA_INT age = __atomic_load_n(e->touchAgeField, __ATOMIC_RELAXED); + JAVA_INT aged = (age == CN1_REF_TOUCHED) ? 0 + : (age < 0x7ffffffe ? age + 1 : age); + __atomic_store_n(e->touchAgeField, aged, __ATOMIC_RELAXED); + if(age == CN1_REF_TOUCHED) { + gcMarkObject(threadStateData, r, JAVA_FALSE); + marked = JAVA_TRUE; + gcMarkDrain(threadStateData); + continue; + } + } +#ifdef CN1_CONSERVATIVE_GC_ROOTS + if(cn1ConservativeResolve((void*)r) != r && !cn1GcImmortalObjContains(r)) { + continue; + } +#endif + { + int mark = __atomic_load_n(&r->__codenameOneGcMark, __ATOMIC_ACQUIRE); + if(mark == -1 || mark >= currentGcMarkValue - 1) { + continue; + } + } + __atomic_store_n(e->referentField, JAVA_NULL, __ATOMIC_RELAXED); + } +#ifdef CN1_GC_CONFORM + cn1RefPhaseNs += cn1GcNowNs() - __r0; +#endif + return marked; +#endif + + // SUB-PASS A: anything read since this cycle aged it is kept, and its referent + // marked. This is not an optimization, it is the reason the pass may clear anything + // at all: a mutator may be holding that referent in a local the collector has already + // walked past. It backs up the SATB load barrier in the accessor rather than + // duplicating it -- the barrier's log can be dropped on an allocation failure, this + // cannot. + // TO A FIXPOINT, because the drain below can DISCOVER references. Marking a touched + // referent traces it, and anything it reaches runs its own mark function -- so an + // object kept alive only by a touched reference can carry further references that were + // not in the list when this pass started. Snapshotting the length once and clearing + // against that snapshot left those unprocessed: reachable, never cleared, and holding + // a referent the sweep went on to free. The loop ends when a drain adds nothing new, + // which it must, since the set only grows and is bounded by the live set. + for(;;) { + n = cn1RefDiscoveredTop; + JAVA_BOOLEAN markedThisRound = JAVA_FALSE; + for(long i = 0 ; i < n ; i++) { + struct CN1RefEntry* e = &cn1RefDiscovered[i]; + JAVA_OBJECT r = __atomic_load_n(e->referentField, __ATOMIC_RELAXED); + if(r == JAVA_NULL || CN1_IS_TAGGED(r)) { + continue; + } + if(__atomic_load_n(e->touchAgeField, __ATOMIC_RELAXED) == CN1_REF_TOUCHED) { + gcMarkObject(threadStateData, r, JAVA_FALSE); + markedThisRound = JAVA_TRUE; +#ifdef CN1_GC_CONFORM + atomic_fetch_add_explicit(&cn1RefKeptTouched, 1, memory_order_relaxed); +#endif + } + } + // Close the round before deciding anything, so sub-pass B reads settled mark + // words. Without this the entries marked above would still look dead to it. + if(markedThisRound) { + marked = JAVA_TRUE; + gcMarkDrain(threadStateData); + } + if(cn1RefDiscoveredTop == n) { + break; // the drain found no further references + } + } + + // A DROPPED LOG ENTRY VOIDS THIS PASS'S EVIDENCE. cn1SatbEnqueue silently discards a + // reference when its stack cannot grow, which for an ordinary store is survivable -- + // the comment there says so -- but not for a referent Reference.get() has already + // handed to a mutator: the enqueue was the only record that it escaped, sub-pass B + // decides purely on mark state, and the final take stays empty so nothing re-opens. + // The result would be the sweep freeing an object a thread is holding. + // + // Nothing here can allocate its way out of that, so the pass declines to clear for the + // cycle. It costs one cycle of reclaim in a situation where the process is already out + // of memory, and it does NOT disable the emergency drop, which clears at DISCOVERY and + // never touches the log. + if(atomic_load_explicit(&cn1SatbDrops, memory_order_relaxed) != cn1RefDropsAtCycleStart) { + // RETAIN, do not merely decline to clear. Returning here leaves every discovered + // referent unmarked AND its field non-null, so the sweep frees objects that live + // References still point at -- which is the dangling read this pass exists to + // prevent, produced by the code meant to prevent it. + // + // This is the second time on this branch that "skip the clear" was mistaken for + // "keep the referent alive"; the note on the unrecorded-discovery path says the + // same thing. Not clearing a weak field does not retain anything, because nothing + // else marks a weak referent -- that is what makes the edge weak. + // RECOVERY, not blanket retention: see cn1GcRecoverAfterDrop. Retaining every + // condemned soft referent here is what would let the allocator spin forever when + // soft-referenced data is the thing exhausting memory. + // + // This was a second hand-written copy of that walk, and it drifted exactly where a + // copy does: the helper was taught that discovery advances TWO counters -- the + // emergency path clears a referent into cn1RefEmergencyCleared without touching + // cn1RefDiscoveredTop, because it exists for when that list cannot grow -- and this + // copy was left comparing the list length alone. It therefore read "nothing new" + // over a freshly written recovery slot and returned without marking what it held. + if(cn1GcRecoverAfterDrop(threadStateData)) { + marked = JAVA_TRUE; + } +#ifdef CN1_GC_CONFORM + cn1RefPhaseNs += cn1GcNowNs() - __r0; +#endif + return marked; + } + + // SUB-PASS B: clear on the referent's liveness alone, then age. + // + // The ageing happens HERE, once per cycle, rather than at discovery -- see the comment + // there. It runs after the clear decision for the same reason: sub-pass A's reading of + // the stamp must not be undone by this pass before the decision that depends on it. + JAVA_BOOLEAN doAge = (cn1RefAgedCycle != currentGcMarkValue) ? JAVA_TRUE : JAVA_FALSE; + for(long i = 0 ; i < n ; i++) { + struct CN1RefEntry* e = &cn1RefDiscovered[i]; + JAVA_OBJECT r = __atomic_load_n(e->referentField, __ATOMIC_RELAXED); + if(r == JAVA_NULL || CN1_IS_TAGGED(r)) { + continue; + } +#ifdef CN1_CONSERVATIVE_GC_ROOTS + // The SAME guard gcMarkObject applies, and for the same reason: a reference + // object kept alive by a stale native-stack word can be dead with a referent + // field that dangles into memory unmapped in an earlier cycle, and reading even + // the mark word of that faults. A pointer that does not resolve is either such + // garbage or an object allocated after this cycle's extent snapshot -- which is + // FRESH, so the grace rule keeps it and there is nothing to clear either way. + if(cn1ConservativeResolve((void*)r) != r && !cn1GcImmortalObjContains(r)) { + continue; + } +#endif + // EXACTLY THE SWEEP'S OWN LIVENESS TEST, and it has to be: clearing a reference + // the sweep then keeps only wastes a cache entry, but FAILING to clear one the + // sweep frees hands get() a dangling pointer, and a dangling read on ParparVM is + // a native crash no Java catch can see. Both halves of the sweep -- the BiBOP + // per-slot walk and the legacy table scan -- free on `mark != -1 && mark < + // currentGcMarkValue - 1`; -1 is the one-cycle grace and currentGcMarkValue - 1 + // is last cycle's slack, and both mean the object survives. + int mark = __atomic_load_n(&r->__codenameOneGcMark, __ATOMIC_ACQUIRE); + if(mark == -1 || mark >= currentGcMarkValue - 1) { + continue; + } +#ifdef CN1_GC_VERIFY + // Fault injection lives with the verifier, and the guard is not decoration: the + // cn1GcFault* family is declared inside CN1_GC_VERIFY, so referencing this one + // unguarded broke every ordinary build while the verifier build -- the only + // configuration where the symbol exists -- went on passing. + if(cn1GcFaultRefClear) { + continue; // fault injection: leave the dead referent in place + } +#endif + // THE STORES ARE SEQUENTIAL, AND DELIBERATELY SO. Review asked for the clearing of + // a referent's aliases to be PUBLISHED atomically as well as decided atomically, + // on the grounds that a mutator landing between two iterations can see one alias + // already null and another not. + // + // It can, and that is not fixable at an acceptable price. Making N stores visible + // as one step needs a lock the reader also takes, and the reader is + // Reference.get() -- the single hot path this whole design is built to keep free + // of one. HotSpot does not do it either: its reference processing clears referents + // one at a time and get() is not synchronised against it. + // + // What the contract actually requires is that the DECISION covers every alias + // together, so the collector never leaves one alias cleared and another live once + // it is finished. That is what the two sub-passes above provide, and it is the + // part that was genuinely broken before them. The residual window is transient and + // self-healing: it lasts only until this loop reaches the other alias, and a get() + // inside it returns a referent that is still valid, because the same read arms the + // barrier and resurrects the object for this cycle. + e->clearedReferent = r; + e->clearedAtPass = cn1RefPass; + __atomic_store_n(e->referentField, JAVA_NULL, __ATOMIC_RELAXED); +#ifdef CN1_GC_CONFORM + atomic_fetch_add_explicit(&cn1RefCleared, 1, memory_order_relaxed); +#endif + } + + // RE-CHECK AFTER A QUIESCE, because the pre-loop check cannot see a drop that has not + // happened yet. A get() starting after that check loads an unmarked referent, and if + // its enqueue fails the counter only moves once the clear above has already decided it + // was safe. The quiesce is what makes the re-read meaningful: every getter registers + // for the duration of its load, so waiting for the in-flight count to reach zero + // guarantees any getter that overlapped this loop has finished and published its drop. + // + // The stores cannot be undone, and do not need to be -- a cleared reference that + // answers null is legal. What must not happen is the OBJECT being freed while a + // mutator holds it, so the recovery marks what was cleared rather than restoring it. + cn1SatbBulkQuiesce(); + if(atomic_load_explicit(&cn1SatbDrops, memory_order_relaxed) != cn1RefDropsAtCycleStart) { + // THE SHARED WALK, not a third hand-written copy. Two earlier copies of this + // fixpoint each drifted from the helper in the same way -- watching only + // cn1RefDiscoveredTop, while the emergency path advances cn1RefEmergencyTop + // instead -- so the loop read "nothing new" over a freshly written recovery slot. + // The helper covers discovered referents, referents already cleared this cycle and + // the emergency array, and iterates on both counters. + if(cn1GcRetainAllReferences(threadStateData)) { + marked = JAVA_TRUE; + } + } + + // AGE, by compare-exchange, so a get() racing this cannot be erased. A plain + // read-modify-write here would reintroduce exactly what deferring the write was meant + // to remove: read 5, a mutator stamps TOUCHED, write 6, and the read has vanished with + // the age not even reset. On a failed exchange the reload sees the mutator's + // CN1_REF_TOUCHED and resets the age to 0, which is what the touch means. + if(doAge) { + for(long i = 0 ; i < n ; i++) { + JAVA_INT* f = cn1RefDiscovered[i].touchAgeField; + JAVA_INT age = __atomic_load_n(f, __ATOMIC_RELAXED); + for(;;) { + // Saturating: an age that wrapped to negative would compare as hotter than + // anything real and make a cold entry immortal. + JAVA_INT aged = (age == CN1_REF_TOUCHED) ? 0 + : (age < 0x7ffffffe ? age + 1 : age); + if(__atomic_compare_exchange_n(f, &age, aged, 0, + __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { + break; + } + // age now holds what the mutator wrote; recompute against it. + } + } + cn1RefAgedCycle = currentGcMarkValue; + } +#ifdef CN1_GC_CONFORM + cn1RefPhaseNs += cn1GcNowNs() - __r0; +#endif + return marked; +} + void codenameOneGCMark() { currentGcMarkValue++; + // PUBLISH THE EPOCH HERE, not only from cn1BibopBeginGcCycle, because that call is + // compiled out under -DCN1_DISABLE_BIBOP and the mirror then stays at 1 forever. + // + // That is not the harmless staleness it looks like, and an earlier comment on this + // branch wrongly called it that. CN1_SATB_REF_KEEP skips a referent whose mark equals + // the epoch; against a frozen epoch it matches nothing from the second collection on, + // so every Reference.get() during a mark enqueues -- which is precisely the unfiltered + // shape measured on this branch to put over 10,000 entries a cycle into the log and + // drive the SATB termination loop into CN1_SATB_MAX_REOPENS every single cycle. A + // filter that silently stops filtering is a performance cliff, not a rounding error. + atomic_store_explicit(&bibopGcEpoch, currentGcMarkValue, memory_order_relaxed); + // Drop the previous cycle's reference list and recompute the soft-retention budget + // from the memory still available. Must precede anything that can mark, because + // cn1GcDiscoverReference reads the budget to decide retention as it goes. + cn1RefBeginCycle(); #ifdef CN1_GC_VERIFY atomic_store_explicit(&cn1GcVerifyMarkActive, 1, memory_order_release); #endif @@ -3073,6 +4185,10 @@ void codenameOneGCMark() { // repeats when it marked something NEW, and marks are monotonic and bounded by the live // set. The common case costs one extra empty cn1SatbTake. int reopens = 0; + // The drop count this cycle has already recovered from. Comparing against the + // cycle-start baseline instead would re-trigger the recovery on every pass, because + // that baseline never moves once a drop has happened. + long recoveredDrops = cn1RefDropsAtCycleStart; __atomic_store_n(&gcSatbTerminating, 1, __ATOMIC_SEQ_CST); for(;;) { for(;;) { @@ -3099,6 +4215,15 @@ void codenameOneGCMark() { gcMarkDrain(d); if(gcMarkNewObjectCount == before) break; // marked nothing new -> closed } + // REFERENCES, here and nowhere else. The strong mark has reached its fixpoint, so + // "unmarked" now means what the sweep will mean by it -- and the barrier is still + // armed, so a get() racing this pass logs its referent and forces the trial clear + // below to find a non-empty log, which re-arms and runs the whole fixpoint (and + // this pass) again. Moving it after the barrier goes down would remove exactly + // that protection and let the sweep free an object a mutator is holding. + // Drains internally between its two sub-passes, which it must -- sub-pass B + // reads the mark words sub-pass A settled -- so there is nothing to drain here. + cn1GcProcessReferences(d); // Trial clear. A store racing it either logged already (caught just below) or // adds an already-marked or fresh reference, which the sweep keeps either way. __atomic_store_n(&gcSatbActive, 0, __ATOMIC_SEQ_CST); @@ -3113,6 +4238,44 @@ void codenameOneGCMark() { cn1GcSatbEntries += n; #endif if(n == 0) { + // A ZERO HERE IS NOT ALWAYS "NOTHING SLIPPED IN". cn1SatbTake reports an + // empty batch both when the log was empty and when it could not grow its + // scratch buffer and threw the entries away -- it records the second case + // in cn1SatbDrops, but this catch runs AFTER cn1GcProcessReferences made + // its last drop check, so nothing would otherwise look at the new value. + // A getter that logged successfully and had its batch discarded here would + // then keep a pointer the following sweep frees. + // + // Retaining is the answer rather than another clear pass: the barrier is + // coming down, so there is no sound basis left for deciding anything is + // dead. + { + long dropsNow = atomic_load_explicit(&cn1SatbDrops, memory_order_relaxed); + if(dropsNow != recoveredDrops) { + // RE-ARM BEFORE TRACING. Retaining marks referents that were white + // and gcMarkDrain then scans them, so with the barrier down those + // objects are grey and unwatched -- and a mutator moving an old + // child out of one in that window logs nothing on either side, which + // is the exact hazard the trial-clear comment above describes. The + // clear is a TRIAL for this reason; recovery is one more thing that + // can turn out to mark something new, so it goes back through the + // fixpoint rather than running underneath a lowered barrier. + recoveredDrops = dropsNow; + __atomic_store_n(&gcSatbActive, 1, __ATOMIC_SEQ_CST); + reopens++; +#ifdef CN1_GC_CONFORM + atomic_fetch_add_explicit(&cn1GcSatbReopens, 1, memory_order_relaxed); +#endif + cn1GcRetainAllReferences(d); + if(reopens < CN1_SATB_MAX_REOPENS) { + continue; // round again with the barrier back up + } + // At the cap: fall through to the same weaker invariant the cap + // documents, with the barrier lowered again below. + __atomic_store_n(&gcSatbActive, 0, __ATOMIC_SEQ_CST); + cn1SatbBulkQuiesce(); + } + } break; // nothing slipped in: closed, barrier down } // The ONLY way out of this loop is the empty catch above. There is deliberately @@ -3167,6 +4330,13 @@ void codenameOneGCMark() { // Draining is the strictly better of the two -- it loses an object only if // a mutator moves a reference out of one specific object during one // specific scan, where not draining loses it with certainty. + // RETAIN WHILE THE BARRIER IS STILL UP. Retention marks referents that + // were white and gcMarkDrain then traces them, so doing it after the clear + // below would leave those objects grey with nothing watching -- the same + // hazard the trial-clear comment describes, and the same one the drop + // recovery was corrected for. Everything known at this point is covered + // here, under the barrier. + cn1GcRetainAllReferences(d); __atomic_store_n(&gcSatbActive, 0, __ATOMIC_SEQ_CST); cn1SatbBulkQuiesce(); { @@ -3179,6 +4349,22 @@ void codenameOneGCMark() { gcMarkDrain(d); } } + // ANYTHING THAT LAST DRAIN DISCOVERED, retained too. + // + // The drain above can newly mark an object whose graph holds a Reference, + // whose mark function then registers a discovery after the retention that + // ran under the barrier. Retaining is all that is left to do with it: + // clearing would need a liveness decision, and the barrier is down. + // + // This last call does trace with the barrier lowered, and that is the + // weaker invariant the cap already documents and already relies on for the + // gcMarkDrain immediately above -- it is not a new exposure, and it is + // bounded by a path measured at 0-4 reopens against a cap of 32. The + // retention that matters happened before the barrier came down. + if(cn1RefDiscoveredTop != 0 + || atomic_load_explicit(&cn1RefEmergencyTop, memory_order_relaxed) != 0) { + cn1GcRetainAllReferences(d); + } break; } } @@ -3782,8 +4968,8 @@ JAVA_BOOLEAN removeObjectFromHeapCollection(CODENAME_ONE_THREAD_STATE, JAVA_OBJE // Something that CHANGES when a collection starts, for callers that need to wait for the // one they just asked for rather than for a handshake that may never reach them. #ifdef CN1_GC_CONFORM -// TEST HOOK. CN1_SIMULATE_ALLOC_FAILURES= makes the next n legacy allocations return -// NULL. The out-of-memory retry path is the one place in this allocator that cannot be +// TEST HOOK. CN1_SIMULATE_ALLOC_FAILURES=[:] makes n legacy allocations return +// NULL, after letting the first through. The out-of-memory retry path is the one place in this allocator that cannot be // reached on a developer machine -- macOS ignores `ulimit -v`, so there is no way to make // calloc fail on demand -- and it has now been the subject of two review findings that // could only be reasoned about. Gated on CN1_GC_CONFORM, like the rest of the QA @@ -3797,14 +4983,45 @@ JAVA_BOOLEAN removeObjectFromHeapCollection(CODENAME_ONE_THREAD_STATE, JAVA_OBJE static _Atomic long cn1SimulatedAllocFailures = 0; _Atomic long cn1AllocRetries = 0; // times the OOM path went round again +// Allocations still to be let through before the failures start. ":" fails n +// allocations AFTER the first skip have succeeded. +// +// Without a skip the hook can only ever fail the FIRST allocations a program makes, which +// is startup -- and a state that exists only at startup cannot exercise anything the +// program builds later. Reaching cn1RefDropAllSoftReferents' effect needs a failure while +// soft referents are actually live, and every attempt without this knob spent its whole +// budget before the first SoftReference existed: the emergency cycles were real and +// visible, and every one of them reported discovered=0. +static _Atomic long cn1SimulatedAllocSkip = 0; + static void cn1AllocFailInit(void) { const char* e = getenv("CN1_SIMULATE_ALLOC_FAILURES"); - long n = e ? atol(e) : 0; + long n = 0, skip = 0; + if(e != 0) { + n = atol(e); + const char* colon = strchr(e, ':'); + if(colon != 0) { + skip = atol(colon + 1); + } + } atomic_store_explicit(&cn1SimulatedAllocFailures, n < 0 ? 0 : n, memory_order_relaxed); + atomic_store_explicit(&cn1SimulatedAllocSkip, skip < 0 ? 0 : skip, memory_order_relaxed); } static JAVA_BOOLEAN cn1ShouldFailAllocation(void) { pthread_once(&cn1AllocFailOnce, cn1AllocFailInit); + // Burn the skip budget first, with the same decrement-only-while-positive discipline + // the failure budget uses and for the same reason. + { + long sk = atomic_load_explicit(&cn1SimulatedAllocSkip, memory_order_relaxed); + while(sk > 0) { + if(atomic_compare_exchange_weak_explicit(&cn1SimulatedAllocSkip, &sk, sk - 1, + memory_order_relaxed, + memory_order_relaxed)) { + return JAVA_FALSE; + } + } + } // Decrement only while positive, so the count cannot go below zero however many // threads race here. long v = atomic_load_explicit(&cn1SimulatedAllocFailures, memory_order_relaxed); @@ -4043,6 +5260,16 @@ JAVA_INT java_lang_System_identityHashCode___java_lang_Object_R_int(CODENAME_ONE BOOL isAppSuspended = 0; #endif +// DEFINED UNCONDITIONALLY, unlike the rest of the BiBOP state below it. It is a plain +// mirror of currentGcMarkValue for mutator-side reads, and the Reference.get() load +// barrier consults it on every call -- so leaving it inside the BiBOP guard meant +// -DCN1_DISABLE_BIBOP failed to link with an undefined _bibopGcEpoch. Hoisting it here is +// the whole fix, and it removes the configuration-dependent macro the barrier briefly +// carried instead. With BiBOP disabled nothing advances it, which costs the barrier some +// extra enqueues and no correctness: the epoch is a filter, and a stale one only ever +// declines to skip. +_Atomic int bibopGcEpoch = 1; + #ifndef CN1_DISABLE_BIBOP // ========================================================================= // BiBOP: non-moving segregated-fits page heap + mark-sweep for SMALL non-array @@ -4201,7 +5428,6 @@ JAVA_INT java_lang_System_identityHashCode___java_lang_Object_R_int(CODENAME_ONE // Non-static: also read/written by the inlined bump fast path (cn1_globals.h). _Atomic long bibopBytesSinceGc = 0; _Atomic long bibopGcTriggerBytes = CN1_BIBOP_GC_TRIGGER_BYTES; -_Atomic int bibopGcEpoch = 1; _Atomic int bibopBypassGeneration[CN1_BIBOP_NUM_CLASSES]; static long bibopCycleAllocatedBytes = 0; // LEGACY bytes charged to the cycle that is starting -- the twin of @@ -4351,9 +5577,9 @@ static void cn1BibopFormatPage(CN1BibopPage* p, int ci) { } void cn1BibopBeginGcCycle(void) { - // Publish the new GC-owned epoch separately for mutators. They must never - // read currentGcMarkValue while the collector increments it concurrently. - atomic_store_explicit(&bibopGcEpoch, currentGcMarkValue, memory_order_relaxed); + // The epoch is published by codenameOneGCMark, which every cycle passes through + // whether or not the page heap is compiled in. It used to be published here, which + // left the mirror frozen under -DCN1_DISABLE_BIBOP; see the note at that store. // Charge allocations racing this mark to the NEXT cycle. The old sweep-end // store lost those bytes and could delay a collection indefinitely under a // sustained allocator. @@ -9303,6 +10529,14 @@ JAVA_OBJECT codenameOneGcMalloc(CODENAME_ONE_THREAD_STATE, int size, struct claz goto cn1GcMallocRetry; } invokedGC = YES; + // An allocation has genuinely failed, so every soft referent goes at the next + // cycle regardless of what the retention ladder thinks. SoftReference's one hard + // guarantee is that all of them are cleared before the VM gives up, and the ladder + // cannot anticipate this on a platform whose headroom probe answers -1 -- it would + // keep re-arming a comfortable budget while this loop collected nothing and + // retried. Raised BEFORE the collection is asked for, so the cycle that request + // starts is the one that honours it. + cn1RefDropAllSoftReferents(); java_lang_System_gc__(getThreadLocalData()); CN1_GC_PARK_CAPTURE(threadStateData); // this park can now last seconds; be scannable threadStateData->threadActive = JAVA_FALSE; @@ -12066,6 +13300,33 @@ void cn1GcProbeCycle(double markMs, double sweepMs, int threw) { atomic_load_explicit(&cn1MonitorEntries, memory_order_relaxed), cn1ImmortalRootsN, cn1FVLive, sideBytes / 1024, residKb); + // java.lang.ref, on its own line so it can be grepped and joined on cyc without + // parsing the wall of fields above. + // + // refMs against graceMs and drainMs is the answer to "does ranking cost more than it + // is worth" on the COLLECTOR side; hit rate against fpKb (which the workload prints) + // answers it on the mutator side. Neither means anything alone. + // + // softBudget is the age, in collections, a soft referent may reach untouched: it is + // CN1_REF_SOFT_RETAIN_MAX with memory to spare, 0 inside the reserve, -1 out of + // budget, and 0x7fffffff means the policy is not trimming at all. Read it FIRST when + // a hit rate looks wrong -- a budget pinned at 0x7fffffff on a host with no + // per-process limit is a measurement of nothing, and is the shape a desktop A/B + // silently takes. + fprintf(stderr, + "[GCREF] v=1 cyc=%d tMs=%lld discovered=%ld weak=%ld retained=%ld" + " keptTouched=%ld cleared=%ld passes=%ld refMs=%.3f softBudget=%d listCap=%ld" + " getsTotal=%ld\n", + currentGcMarkValue, cn1GcProbeElapsedMs(), + atomic_load_explicit(&cn1RefDiscoveries, memory_order_relaxed), + atomic_load_explicit(&cn1RefWeak, memory_order_relaxed), + atomic_load_explicit(&cn1RefRetained, memory_order_relaxed), + atomic_load_explicit(&cn1RefKeptTouched, memory_order_relaxed), + atomic_load_explicit(&cn1RefCleared, memory_order_relaxed), + cn1RefPasses, cn1RefPhaseNs / 1e6, + atomic_load_explicit(&cn1SoftRetainCycles, memory_order_relaxed), + cn1RefDiscoveredCap, + atomic_load_explicit(&cn1RefGets, memory_order_relaxed)); fflush(stderr); // Per-CYCLE, so reset after reporting. A running total cannot show a trend. cn1GcProbeResetPhases(); @@ -12467,6 +13728,20 @@ static void cn1ReportAllocProfile(void) { } #endif +// Reference.get() calls for the WHOLE run, printed at exit. +// +// The per-cycle [GCREF] line cannot answer "how often does this workload call get()": +// it only prints when a collection happens, so a get()-heavy but allocation-light phase +// -- exactly the shape that costs the load barrier the most -- leaves the last line +// stranded early in the run. Costing the barrier off that number understates the call +// count and therefore overstates the nanoseconds per call; measured, it read 153,408 for +// a run that made millions. +static void cn1ReportRefGets(void) { + fprintf(stderr, "[GCREF-TOTAL] gets=%ld\n", + atomic_load_explicit(&cn1RefGets, memory_order_relaxed)); + fflush(stderr); +} + static void cn1ReportStalls(void) { long long wallMs = cn1GcProbeElapsedMs(); // Mutator-only, to match the thread count it is divided by; the per-cause table below @@ -12782,6 +14057,8 @@ void initConstantPool() { #ifdef CN1_GC_CONFORM atexit(cn1ReportAllocProfile); #endif + + atexit(cn1ReportRefGets); #ifdef CN1_CONSERVATIVE_GC_ROOTS // The self test sorts the conservative extent table, which only exists on // this arm. Calling it under CN1_GC_CONFORM alone does not compile, so diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java index 9d973069d90..5701065e0b8 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java @@ -37,6 +37,43 @@ */ public class ByteCodeClass { + /** + * The one class whose reference field the collector owns rather than traces. + * + *

{@code java.lang.ref.Reference.objReference} is the referent of every + * WeakReference and SoftReference in the program. Emitting the ordinary + * {@code gcMarkObject} for it would make it a STRONG edge -- which is exactly + * what ParparVM did until this opt-out existed, so that a "weak" reference + * pinned its referent for the life of the process and every cache built on + * {@code Display.createSoftWeakRef} was unbounded. The two emission sites + * below replace that with a weak edge and the load barrier that makes reading + * one safe while a concurrent mark is running.

+ * + *

Matched by name rather than by any annotation because the class is part + * of the VM's own {@code java.lang} surface: there is nowhere to hang an + * annotation that {@code vm/JavaAPI} and {@code Ports/CLDC11} would both + * accept, and a marker interface would be one more thing to keep in step. + * {@code Reference} is final in practice -- its constructor is package + * private -- so the set of classes this can apply to is closed.

+ */ + static final String REFERENCE_CLASS = "java_lang_ref_Reference"; + + /** The referent field within {@link #REFERENCE_CLASS}. */ + static final String REFERENCE_REFERENT_FIELD = "objReference"; + + /** + * True for the one field whose GC treatment and read accessor are special + * cased below. Both call sites must agree, hence the shared predicate: + * suppressing the mark without adding the barrier produces a collector that + * frees a referent a mutator is holding, and adding the barrier without + * suppressing the mark produces a weak reference that is still strong. + */ + private static boolean isReferenceReferent(String owningClass, ByteCodeField fld) { + return REFERENCE_CLASS.equals(owningClass) + && REFERENCE_REFERENT_FIELD.equals(fld.getFieldName()) + && REFERENCE_CLASS.equals(fld.getClsName()); + } + /** * @param isAnonymous the isAnonymous to set */ @@ -1000,7 +1037,41 @@ public String generateCCode(List allClasses) { b.append("_"); b.append(fld.getFieldName()); b.append("(JAVA_OBJECT __cn1T) {\n ").append(nullCheck).append(" "); - if (fld.isVolatile()) { + if(isReferenceReferent(clsName, fld)) { + // Reference.get() compiles into this accessor, and reading a weak referent + // while a concurrent mark is running needs a barrier an ordinary field read + // does not. + // + // ONE LOAD, and everything below acts on that value. The collector clears a + // reference after the strong mark reaches its fixpoint but with mutators + // still running, so a thread whose stack was scanned and released early can + // take the referent out of here and hold it in a local the collector has + // walked past -- neither marked nor fresh, the one case the sweep's "already + // marked or FRESH" invariant does not cover. Enqueuing puts it back in the + // snapshot, and the trial clear of gcSatbActive then finds a non-empty log, + // re-arms, marks it, and leaves the reference alone. + // + // REGISTERING BEFORE THE LOAD, and holding it across, is what makes that + // sound. Any flag sampled before registering can go stale in the gap: a + // thread that read the flag as 0 -- or read it as 1 and was then descheduled + // before registering -- can come away with an unmarked referent nothing + // enqueued, while the collector finishes termination and sweeps it. + // CN1_REF_LOAD_BEGIN registers first and answers afterwards, so the + // collector's quiesce cannot complete anywhere inside this accessor. + b.append("JAVA_BOOLEAN __cn1RefActive = CN1_REF_LOAD_BEGIN();\n "); + b.append(fld.getCDefinition()).append(" __cn1Ref = __atomic_load_n(&((struct obj__") + .append(clsName).append("*)__cn1T)->") + .append(fld.getClsName()).append("_").append(fld.getFieldName()) + .append(", __ATOMIC_RELAXED);\n "); + b.append("CN1_SATB_REF_KEEP(__cn1RefActive, __cn1Ref);\n "); + // The touch stamp, and the entire per-read cost of ranking soft references + // by use: a store of an immediate. Unconditional rather than guarded by a + // "did it change" test, because the branch would cost more than the store. + b.append("__atomic_store_n(&((struct obj__").append(clsName).append("*)__cn1T)->") + .append(REFERENCE_CLASS).append("_cn1TouchAge, CN1_REF_TOUCHED, __ATOMIC_RELAXED);\n "); + b.append("CN1_REF_LOAD_END();\n "); + b.append("return __cn1Ref;\n}\n\n"); + } else if (fld.isVolatile()) { b.append("return atomic_load_explicit(&((struct obj__"); b.append(clsName); b.append("*)__cn1T)->"); @@ -1033,12 +1104,23 @@ public String generateCCode(List allClasses) { b.append("CN1_WRITE_BARRIER(__cn1T, __cn1Val); "); // SATB deletion barrier: preserve the reference being overwritten for the // current mark cycle. No-op (single flag load) outside GC. - b.append("CN1_SATB_DELETE(&((struct obj__").append(clsName).append("*)__cn1T)->") + // The referent takes the ATOMIC deletion barrier: the collector stores + // JAVA_NULL into that field concurrently, so the generic macro's plain + // volatile read would leave the pair a mixed atomic/non-atomic access. + b.append(isReferenceReferent(clsName, fld) ? "CN1_SATB_DELETE_REF" : "CN1_SATB_DELETE") + .append("(&((struct obj__").append(clsName).append("*)__cn1T)->") .append(fld.getClsName()).append("_").append(fld.getFieldName()).append("); "); } else { b.append(" __cn1Val, JAVA_OBJECT __cn1T) {\n ").append(nullCheck).append(" "); } - if (fld.isVolatile()) { + if(isReferenceReferent(clsName, fld)) { + // Reference.clear() and the constructor both land here, and the collector + // stores JAVA_NULL into the same word concurrently. Atomic for the reason + // spelled out on the getter above. + b.append("__atomic_store_n(&((struct obj__").append(clsName).append("*)__cn1T)->") + .append(fld.getClsName()).append("_").append(fld.getFieldName()) + .append(", __cn1Val, __ATOMIC_RELAXED);\n}\n\n"); + } else if (fld.isVolatile()) { b.append("atomic_store_explicit(&((struct obj__"); b.append(clsName); b.append("*)__cn1T)->"); @@ -1086,6 +1168,27 @@ public String generateCCode(List allClasses) { b.append("*)objToMark;\n"); for(ByteCodeField fld : fullFieldList) { if(!fld.isStaticField() && fld.isObjectType() && fld.getClsName().equals(clsName)) { + if(isReferenceReferent(clsName, fld)) { + // THE REFERENT IS NOT TRACED. Handing it to gcMarkObject here is + // what made every WeakReference strong; instead the collector is + // told the reference exists and is given the addresses it needs to + // decide, once the strong mark has closed, whether to keep the + // referent or clear the field. + // + // Addresses rather than the object, deliberately: cn1_globals.m is a + // fixed template compiled beside whatever the translator emitted, and + // it cannot name `struct obj__java_lang_ref_Reference` -- the class is + // absent from any program that never uses a reference, and including + // its generated header would make the runtime fail to build for those. + // Passing field pointers keeps the layout knowledge on this side, + // where it is generated from the layout itself. + b.append(" cn1GcDiscoverReference(threadStateData, objToMark, force, &objInstance->"); + b.append(fld.getClsName()).append("_").append(fld.getFieldName()); + b.append(", &objInstance->").append(REFERENCE_CLASS).append("_cn1TouchAge"); + b.append(", &objInstance->").append(REFERENCE_CLASS).append("_cn1AgedCycle"); + b.append(", objInstance->").append(REFERENCE_CLASS).append("_cn1Strength);\n"); + continue; + } b.append(" gcMarkObject(threadStateData, "); if (fld.isVolatile()) { b.append("atomic_load_explicit(&objInstance->"); diff --git a/vm/CLAUDE.md b/vm/CLAUDE.md index 175916114f0..caaeb6e40f6 100644 --- a/vm/CLAUDE.md +++ b/vm/CLAUDE.md @@ -560,6 +560,88 @@ A/B, and is what the gate's third scenario re-injects to prove it can fail. Reach for `CN1_SIMULATE_PROC_MEMORY_LIMIT=` to exercise any of this off-device — without it the budgeted pacing path never runs, which is how the original bug survived. +## java.lang.ref: what it cost, and what the ranking did not buy + +The collector clears references itself. The referent lives in `java.lang.ref.Reference` +and the translator does NOT emit a `gcMarkObject` for it +(`ByteCodeClass.isReferenceReferent`): it emits `cn1GcDiscoverReference`, which hands the +collector the field addresses and decides soft retention on the spot. Clearing happens in +`cn1GcProcessReferences`, inside the SATB termination loop, using the sweep's own liveness +test -- `mark != -1 && mark < currentGcMarkValue - 1`, both halves of the sweep agree on +it. Clearing a reference the sweep keeps wastes a cache entry; failing to clear one it +frees is a dangling read, which on this VM is a native crash no Java catch can see. + +**The clear pass must run with the SATB barrier still ARMED.** A thread scanned and +released early can pull a referent out through `get()` and hold it in a local the +collector has already walked past, and that referent is then neither marked nor fresh -- +the one case the sweep's "already marked or FRESH" invariant does not cover. `get()` +therefore carries a load barrier, emitted into +`get_field_java_lang_ref_Reference_objReference`, and a racing read makes the trial clear +of `gcSatbActive` find a non-empty log, which re-arms and re-runs the fixpoint and this +pass with it. + +**Filter that barrier or the collector stops converging.** Logging every referent read is +not a cost, it is a failure: `cn1SatbEnqueue` takes a mutex per accepted reference and +`get()` on a hot cache is called far more often than any store barrier sees. Measured on +`RefPolicy` before the filter existed -- over 10,000 log entries per cycle and +`CN1_SATB_MAX_REOPENS` (32) reached on EVERY cycle. `CN1_SATB_REF_LOAD` skips referents +already marked this epoch or fresh, which are exactly the ones the clear pass would refuse +to clear: passes 32 -> 1, keptTouched ~10,000 -> ~330, refMs 0.06 -> 0.005. + +### The measurement, and the three ways it lied first + +`vm/benchmarks/src/com/bench/RefPolicy.java` + `ab-refs.sh`, arms `-DCN1_NO_WEAK_REFS` +(references strong, what this VM did before) and `-DCN1_REF_POLICY=0|1|2` +(pressure-triggered all-or-nothing / never clear / ranked by age). Five interleaved reps, +`CN1_SIMULATE_PROC_MEMORY_LIMIT`, checksums identical across every arm: + +| ceiling | arm | hit rate | footprint | refMs | % of mark | weak cleared | +|---|---|---|---|---|---|---| +| 128MB | noweak | 97.44% | 63.5MB | 0.002 | 0.00% | 0/256 | +| 128MB | pressure | 84.99% | 63.1MB | 2.760 | 2.46% | 255/256 | +| 128MB | never | 97.44% | 63.8MB | 1.289 | 1.88% | 255/256 | +| 128MB | ranked | 96.77% | 62.6MB | 1.197 | 1.79% | 255/256 | +| 160MB | pressure | 87.99% | 91.0MB | 2.611 | 16.32% | 255/256 | +| 160MB | ranked | 97.44% | 82.1MB | 0.421 | 3.63% | 255/256 | + +Read it in this order. **References themselves are unambiguous**: 255/256 unreachable +referents reclaimed against 0/256, for 1.8-4% of mark time and a `vm/benchmarks` geomean +of 1.011 over 12 interleaved reps against master. **The pressure-triggered arm is strictly +dominated** -- it gives up 12 points of hit rate and saves no footprint at all, and at +160MB it is worse on BOTH axes. That arm is the model of the iOS port's +`didReceiveMemoryWarning -> flushSoftRefMap`, so it is the thing being replaced, not a +strawman. **The ranking buys nothing over never-clearing here**: same hit rate, ~1MB less. +It is defensible because it costs almost nothing and because it dominates the pressure +arm, not because this measurement shows it winning. + +Three wrong conclusions were drawn from single runs before that table existed, and each +survived until the data contradicted it: + +- **"Ranking is the difference between finishing and not."** True of the outcome, wrong + about the cause: the arms that did not finish were not out of memory. `sample` on a + wedged process put the mutator 100% in `cn1PacingPark` at 44MB of a 96MB ceiling. The + chain is retain-everything -> the collector cannot shrink the live set -> the pacing loop + parks the mutator to hold the budget. Reach for the stacks first, as the demand-signal + note above already says. +- **"The pressure arm fails because all-or-nothing thrashes."** It never fired at all. + Its trigger was below the pacing reserve, and defending that reserve is what the pacing + loop DOES, so headroom converges on the trigger and stops falling. **Any + pressure-triggered cache policy on this collector has that trap waiting: it waits for a + signal the collector exists to suppress.** The second attempt then wrote the bands as + multiples of the reserve, where `reserve * 4` IS the whole budget, so the top band was + unreachable and the arm fired always. Write bands as explicit fractions; reachability is + then visible on inspection. +- **"The non-trimming arms collapse."** `never` was 4x FASTER than `noweak` at 2,000 + accesses and 100x slower at 6,000. Below roughly 1.8x the cache size this workload is + **bistable** -- once pacing engages, throughput drops two orders of magnitude, and + whether a run falls in is timing-sensitive. Single runs there measure the coin. If that + regime is what you want, count how many of N runs complete; do not time one. + +**What is still not measured.** Nothing here separates ranking by RECENCY from "trims at +all" -- there is no random-eviction arm at a matched rate, so the LRU claim is unproven, +only the trimming claim. And the 64MB-cache-against-a-128MB-budget shape is a choice made +to stress the policy, not a measured property of any app. + ## GC latency: the mutator's clock, not the collector's Everything above measures MEMORY. The reporter of #5537 ended up passing all of it and still diff --git a/vm/JavaAPI/src/java/lang/ref/Reference.java b/vm/JavaAPI/src/java/lang/ref/Reference.java index 33521cb931d..3dd585c77ab 100644 --- a/vm/JavaAPI/src/java/lang/ref/Reference.java +++ b/vm/JavaAPI/src/java/lang/ref/Reference.java @@ -25,26 +25,113 @@ /** * Abstract base class for reference objects. This class defines the operations common to all reference objects. Because reference objects are implemented in close cooperation with the garbage collector, this class may not be subclassed directly. * Since: JDK1.2, CLDC 1.1 + * + *

The four fields below are a contract with the collector, not ordinary + * state. Three separate places know their names literally, and renaming one + * without the others produces a build that compiles and silently stops + * collecting -- or, worse, one that clears a reference whose referent is still + * in use:

+ * + *
    + *
  • {@code ByteCodeClass} suppresses the usual {@code gcMarkObject} for + * {@code objReference} in {@code __GC_MARK_java_lang_ref_Reference} and + * emits a {@code cn1GcDiscoverReference} call in its place, handing the + * collector the addresses of these fields. That suppression is what makes + * the referent a weak edge instead of a strong one.
  • + *
  • The same class adds the SATB load barrier and the touch stamp to + * {@code get_field_java_lang_ref_Reference_objReference}, which is the + * accessor every {@code get()} below compiles into.
  • + *
  • {@code cn1GcProcessReferences} in {@code cn1_globals.m} reads and writes + * all four through those addresses.
  • + *
+ * + *

Consequently {@code objReference} must stay the ONLY object-typed field in + * this class: the translator's opt-out is keyed on the class and field name, and + * a second reference field would be traced strongly with nothing to say so.

*/ public abstract class Reference{ + /** + * The referent. Deliberately package private and declared HERE rather than in + * WeakReference, so that one translator opt-out and one collector pass cover + * every subclass. + */ + Object objReference; + + /** + * Cycles since the last {@link #get()}, maintained by the collector, and the + * "hot" input to the soft-reference retention policy. + * + *

{@code TOUCHED} (-1) is written by the field accessor on every read -- + * a store of an immediate, which is the whole per-get cost of ranking, and + * why the ranking is done this way rather than by reading a clock or an + * epoch counter. The collector converts a -1 back to 0 and increments + * everything else once per cycle, so the value is an age in collections.

+ * + *

It also carries the safety property that lets the collector clear a + * reference at all while mutators run: see the discussion of + * {@code cn1GcProcessReferences}.

+ */ + // Starts at 0 -- "read this cycle", not TOUCHED. TOUCHED means "read since the + // collector last aged this", and a reference nothing has called get() on yet has not + // been; starting there would make the clear pass treat every newly discovered + // reference as freshly used and mark its referent for a cycle, weak ones included. + // Zero already reads as maximally hot to the soft-retention test, which is what a new + // cache entry should be. + int cn1TouchAge; + + /** + * {@link #STRENGTH_WEAK} or {@link #STRENGTH_SOFT}, set by the subclass + * constructor. + * + *

The collector needs to tell the two apart and deliberately does NOT do + * it by comparing class pointers: that would make the runtime depend on a + * generated class symbol that the dead-code pass is entitled to remove, and + * would answer wrongly for a user-written subclass of either.

+ */ + int cn1Strength; + + /** + * The mark value of the cycle that last aged this reference, so that a + * reference reached more than once in a cycle ages exactly once. + * + *

Being reached twice is normal rather than exceptional: force-marking + * re-runs mark functions over already-marked objects once per statics pass + * and again for the constant pool, so the collector sees popular references + * several times per cycle.

+ */ + int cn1AgedCycle; + + /** {@link #cn1TouchAge} value meaning "read since the collector last aged this". */ + static final int TOUCHED = -1; + + /** A {@link #cn1Strength} that is never retained once the referent is unreachable. */ + static final int STRENGTH_WEAK = 0; + + /** A {@link #cn1Strength} that is retained while recently used and memory allows. */ + static final int STRENGTH_SOFT = 1; + + Reference(Object ref, int strength) { + this.objReference = ref; + this.cn1Strength = strength; + } + /** * Clears this reference object. */ public void clear(){ - clearImpl(); + objReference = null; } /** * Returns this reference object's referent. If this reference object has been cleared, either by the program or by the garbage collector, then this method returns null. + * + *

This compiles to {@code get_field_java_lang_ref_Reference_objReference}, + * which the translator gives a SATB load barrier and the touch stamp. Both + * belong on the ACCESSOR rather than here: every read of the field goes + * through it, including any the optimizer generates, whereas a barrier + * written in Java would cover only the one call site below.

*/ public java.lang.Object get(){ - return getImpl(); - } - - Object getImpl() { - return null; - } - - void clearImpl() { + return objReference; } } diff --git a/vm/JavaAPI/src/java/lang/ref/SoftReference.java b/vm/JavaAPI/src/java/lang/ref/SoftReference.java new file mode 100644 index 00000000000..602fdc7a259 --- /dev/null +++ b/vm/JavaAPI/src/java/lang/ref/SoftReference.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2012, Codename One 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. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +package java.lang.ref; +/** + * A reference the collector keeps while the referent is being used and memory + * allows, and clears before the process runs out. + * + *

This is the reference a CACHE wants, and the difference from + * {@link WeakReference} is not a detail: a weak referent is dropped by the first + * collection after the last ordinary reference to it, which for a decoded bitmap + * behind an {@code EncodedImage} means the cache is emptied faster than it can be + * filled and never hits. A soft reference survives that collection, and the one + * after it, for as long as something is still asking for it.

+ * + *

Retention is ranked by use. The collector ages every reference by one + * on each cycle and resets the age to zero when {@code get()} is called, then + * keeps a soft referent while that age is within a budget it recomputes each + * cycle from the memory still available to the process. So a cache under memory + * pressure loses its cold entries first rather than all of them at once, and an + * entry read on every frame is the last thing to go.

+ * + *

The whole per-read cost of that ranking is a store of a constant into the + * reference, which is why it is ranked by age rather than by reading a clock. + * The decision itself is made once per cycle, when the collector first reaches + * the reference, so ranking costs the mark nothing beyond the walk it was + * already doing -- deciding it afterwards would mean computing a second + * reachability closure over the retained set, on a mark that already spends most + * of its time in the grace pass.

+ */ +public class SoftReference extends java.lang.ref.Reference{ + // NOTHING IN THE PORTS CONSTRUCTS ONE YET, and that is the intended state here. + // Display.createSoftWeakRef still answers with a WeakReference, and the iOS port still + // overrides it with a Hashtable that pins entries until a memory warning; migrating + // those is a separate change, for the reasons recorded at + // CodenameOneImplementation.createSoftWeakRef. What this class and the collector work + // beside it deliver is the mechanism and the measurement that justifies it -- an + // application may construct one today and get the ranked behaviour. + + + /** + * Creates a new soft reference that refers to the given object. + */ + public SoftReference(java.lang.Object ref){ + super(ref, STRENGTH_SOFT); + } +} diff --git a/vm/JavaAPI/src/java/lang/ref/WeakReference.java b/vm/JavaAPI/src/java/lang/ref/WeakReference.java index dbaef8f6b68..a1af9be37e1 100644 --- a/vm/JavaAPI/src/java/lang/ref/WeakReference.java +++ b/vm/JavaAPI/src/java/lang/ref/WeakReference.java @@ -25,36 +25,37 @@ /** * This class provides support for weak references. Weak references are most often used to implement canonicalizing mappings. Suppose that the garbage collector determines at a certain point in time that an object is weakly reachable. At that time it will atomically clear all the weak references to that object and all weak references to any other weakly- reachable objects from which that object is reachable through a chain of strong and weak references. * Since: JDK1.2, CLDC 1.1 + * + *

The referent lives in {@link Reference}, and the collector clears it once + * the object is reachable no other way. Two properties of THIS collector make + * that best-effort rather than prompt, and both are legal -- the contract says a + * reference "may" be cleared, never that it must be:

+ * + *
    + *
  • A newly allocated object is kept unconditionally for one cycle by the + * sweep's grace rule, so a referent is never cleared in the cycle it dies.
  • + *
  • The root scan reads native C stacks conservatively, so a stale machine + * word that happens to look like the referent keeps it marked. {@code get()} + * will occasionally keep answering an object nothing references any more.
  • + *
+ * + *

Historical note worth keeping, because both halves were real shipped bugs. + * This class first held its referent in an ordinary field, which the translator + * traced like any other -- so a "weak" reference was strong and the caches built + * on {@code CodenameOneImplementation.createSoftWeakRef} pinned every decoded + * bitmap for the life of the process. Before that, the constructor assigned the + * field to itself ({@code this.objReference = objReference}) and dropped the + * argument, so every reference was born empty and {@code get()} was hardwired to + * null: the same caches could then never hit. Note the failure modes are exact + * opposites, which is why this class needs tests that pin BOTH ends -- that the + * referent is answered while it is reachable, and that it stops being answered + * once it is not.

*/ public class WeakReference extends java.lang.ref.Reference{ - private Object objReference; - /** * Creates a new weak reference that refers to the given object. - *

- * Note that ParparVM's collector has no notion of a weak root: it never - * clears this field, so the referent lives exactly as long as the - * reference object does and {@link #get()} keeps answering it until - * {@link Reference#clear()} is called by hand. That is a legal (if - * pessimistic) implementation of the contract -- "may be cleared" is not - * "must be cleared" -- and it is what the callers need. What is NOT legal - * is the reverse: this constructor used to assign the field to itself - * ({@code this.objReference = objReference}) and drop {@code ref} on the - * floor, so every reference was born empty and {@code get()} was hardwired - * to null. Everything built on - * {@code CodenameOneImplementation.createSoftWeakRef} -- the EncodedImage - * decode cache, Image's scale cache, Border's round-rect cache -- was then - * a cache that could never hit. */ public WeakReference(java.lang.Object ref){ - this.objReference = ref; - } - - Object getImpl() { - return objReference; - } - - void clearImpl() { - objReference = null; + super(ref, STRENGTH_WEAK); } } diff --git a/vm/benchmarks/ab-refs.sh b/vm/benchmarks/ab-refs.sh new file mode 100755 index 00000000000..5d88b83fcf7 --- /dev/null +++ b/vm/benchmarks/ab-refs.sh @@ -0,0 +1,165 @@ +#!/bin/bash +# A/B the java.lang.ref retention policies (CN1_REF_POLICY) against the behaviour +# they replaced. +# +# noweak -DCN1_NO_WEAK_REFS references are traced strongly and never cleared. +# This is what ParparVM did before references existed, +# and what the iOS port's soft-reference table still +# does between memory warnings. +# pressure -DCN1_REF_POLICY=0 keep every soft referent until headroom drops into +# the reserve, then drop all of them at once -- the +# GC-integrated form of didReceiveMemoryWarning -> +# flushSoftRefMap. +# never -DCN1_REF_POLICY=1 never clear a soft referent. The upper bound on hit +# rate and on footprint. +# ranked -DCN1_REF_POLICY=2 clear by age since the last get(). The default. +# +# THE POINT OF ALL FOUR IS THAT NEITHER AXIS MEANS ANYTHING ALONE. "never" wins the +# hit rate by keeping everything and "pressure" wins the footprint by keeping nothing; +# ranking is worth its one int field and one store per get() only if it holds a higher +# hit rate than "pressure" AT A COMPARABLE FOOTPRINT. The table below prints both for +# every arm at every ceiling so that comparison cannot be made one column at a time. +# +# ./ab-refs.sh [reps] [ceilingMB ...] +# +# REF_WORKLOAD passes argv to the driver ("keys payloadBytes accesses churn"), and +# REF_TIMEOUT bounds a single run. +# +# CHOOSE CEILINGS WHERE EVERY ARM COMPLETES RELIABLY. Below roughly 1.8x the cache +# size this workload becomes BISTABLE: once the retained set stops the collector +# freeing enough, the process-budget pacing loop parks the mutator and throughput +# collapses by two orders of magnitude, and whether a given run falls into that state +# is timing-sensitive. Measured on this host, the same arm took 936ms and 391s on +# neighbouring workload sizes, and the arms ordered differently each time. Single runs +# in that regime measure the coin, not the policy -- if the tight regime is what you +# want to characterise, count how many of N runs complete rather than timing one. +# +# Requirements: JDK_8_HOME, Maven, clang. +set -e +cd "$(dirname "$0")" +REPS="${1:-5}"; shift || true +CEILINGS="${*:-96 128 192}" +LTO="${CN1_BENCH_LTO--flto=thin}" +mkdir -p target/ab-refs + +# CN1_GC_CONFORM is in every arm. It changes no allocator behaviour -- unlike +# CN1_GC_VERIFY, which forces cn1BibopReleaseOffset() to 0 and compiles out page +# release and the major sweep, so a footprint measured in a verifier build is a +# measurement of the verifier. It is what supplies [GCREF], and it is present in ALL +# arms so the arms differ in one thing only. +build() { # name, flags + CN1_BENCH_CFLAGS="$LTO -DCN1_GC_CONFORM $2" ./translate-and-build.sh RefPolicy \ + "target/ab-refs/$1" >"target/ab-refs/$1.build.log" 2>&1 \ + || { echo "BUILD FAILED: $1"; tail -25 "target/ab-refs/$1.build.log"; exit 1; } + echo "built $1" +} +build noweak "-DCN1_NO_WEAK_REFS" +build pressure "-DCN1_REF_POLICY=0" +build never "-DCN1_REF_POLICY=1" +build ranked "-DCN1_REF_POLICY=2" + +REPS="$REPS" CEILINGS="$CEILINGS" python3 - <<'EOF' +import subprocess, re, os, sys, statistics + +ARMS = ["noweak", "pressure", "never", "ranked"] +reps = int(os.environ["REPS"]) +ceilings = [int(c) for c in os.environ["CEILINGS"].split()] + +def run(arm, ceiling_mb): + env = dict(os.environ) + # Scrub every CN1_* the caller may have exported. An inherited CN1_GC_PROBE or + # CN1_SIMULATE_PROC_MEMORY_LIMIT would silently make two arms incomparable, which + # reads as a policy difference rather than as a mistake. + for k in [k for k in env if k.startswith("CN1_")]: + del env[k] + env["CN1_SIMULATE_PROC_MEMORY_LIMIT"] = str(ceiling_mb * 1024 * 1024) + env["CN1_GC_PROBE"] = "1" + p = subprocess.run([f"target/ab-refs/{arm}"] + os.environ.get("REF_WORKLOAD", "").split(), + capture_output=True, text=True, env=env, + timeout=float(os.environ.get("REF_TIMEOUT", "600"))) + # A NONZERO EXIT INVALIDATES THE SAMPLE, however complete the output looks. The metrics + # are printed before the process ends, so a VM that corrupts its heap and dies in an + # atexit handler still emits RESULT and the whole table -- and without this the harness + # would publish checksum-matched medians from a crashed run and exit 0. + if p.returncode != 0: + raise SystemExit(f"{arm}@{ceiling_mb}MB: exited {p.returncode}; sample rejected\n" + + p.stdout[-2000:] + "\n" + p.stderr[-2000:]) + # THE WEAK PHASE MUST ACTUALLY HAVE CLEARED SOMETHING (or, for noweak, nothing). + # The checksums are deliberately independent of retention policy, so they agree just + # as well when an arm silently keeps every referent strong -- which is precisely the + # regression that matters here, and it would have published medians and exited 0. + # Asserting on the numerator being nonzero rather than on 256/256: clearing is + # best-effort by construction, since a stale word on the conservative native stack + # pins a referent, and the observed figure is 255/256. + m = re.search(r'^WEAK_DEAD_CLEARED=(\d+)/(\d+)', p.stdout, re.M) + if not m: + raise SystemExit(f"{arm}@{ceiling_mb}MB: no WEAK_DEAD_CLEARED in output") + got = int(m.group(1)) + if arm == "noweak" and got != 0: + raise SystemExit(f"noweak@{ceiling_mb}MB: cleared {got} referent(s); with " + f"CN1_NO_WEAK_REFS the referent is a strong edge and nothing " + f"may be cleared -- the arm is not the control it is read as") + if arm != "noweak" and got == 0: + raise SystemExit(f"{arm}@{ceiling_mb}MB: cleared 0 of {m.group(2)} dead " + f"referents; this arm never exercised clearing, so its hit rate " + f"and footprint describe no policy") + out = p.stdout + def num(key, default=None): + m = re.search(rf'^{key}=(-?\d+)', out, re.M) + if m: return int(m.group(1)) + if default is not None: return default + raise SystemExit(f"{arm}@{ceiling_mb}MB: no {key} in output\n{out}\n{p.stderr[-2000:]}") + refms, cleared, retained = [], 0, 0 + for m in re.finditer(r'\[GCREF\].*?cleared=(\d+).*?refMs=([\d.]+)', p.stderr): + cleared += int(m.group(1)); refms.append(float(m.group(2))) + for m in re.finditer(r'\[GCREF\].*?retained=(\d+)', p.stderr): + retained += int(m.group(1)) + markms = [float(m.group(1)) for m in re.finditer(r'markMs=([\d.]+)', p.stderr)] + return { + "hit_ppm": num("HIT_RATE_PPM"), "fp_kb": num("FINAL_FOOTPRINT_KB"), + "checksum": num("RESULT"), + "weak_cleared": out.split("WEAK_DEAD_CLEARED=")[1].split("\n")[0] if "WEAK_DEAD_CLEARED=" in out else "?", + "refms": sum(refms), "markms": sum(markms), "cleared": cleared, "retained": retained, + } + +results = {(a, c): [] for a in ARMS for c in ceilings} +for rep in range(reps): + # INTERLEAVED. Physical footprint moves with the host's own memory pressure, so two + # soaks taken minutes apart measure the machine; every arm has to see the same + # machine state, which only holds if they alternate inside one session. + for c in ceilings: + for a in ARMS: + results[(a, c)].append(run(a, c)) + print(f"rep {rep+1}/{reps}", flush=True) + +# Checksum parity. A retention policy decides WHEN a payload is rebuilt, never what it +# contains, and the driver accumulates what it read rather than what it rebuilt -- so a +# checksum that moves across arms is a correctness bug, not a policy difference. +sums = {(a, c): {r["checksum"] for r in rs} for (a, c), rs in results.items()} +allsums = set().union(*sums.values()) +if len(allsums) != 1: + print("\nCHECKSUM MISMATCH across arms (correctness bug):") + for k, v in sorted(sums.items()): + print(f" {k}: {sorted(v)}") + sys.exit(1) + +med = lambda vals: statistics.median(vals) +print(f"\nchecksum {allsums.pop()} identical across every arm and ceiling" + f" ({reps} interleaved reps, medians below)") +for c in ceilings: + print(f"\n--- process ceiling {c} MB " + "-" * 46) + # refMs and markMs are TOTALS over the run's cycles, so their ratio is the share + # of collector time the reference phase costs -- which is the question. The + # absolute ms is kept beside it only so a suspiciously round ratio can be checked. + print(f"{'arm':<10}{'hit rate %':>12}{'footprint MB':>14}{'refMs total':>13}" + f"{'% of mark':>11}{'softCleared':>12}{'weak':>10}") + for a in ARMS: + rs = results[(a, c)] + hit = med([r["hit_ppm"] for r in rs]) / 10000.0 + fp = med([r["fp_kb"] for r in rs]) / 1024.0 + ref = med([r["refms"] for r in rs]) + mark = med([r["markms"] for r in rs]) + share = (100.0 * ref / mark) if mark > 0 else 0.0 + print(f"{a:<10}{hit:>11.2f}%{fp:>14.1f}{ref:>13.3f}{share:>10.2f}%" + f"{med([r['cleared'] for r in rs]):>12.0f}{rs[0]['weak_cleared']:>10}") +EOF diff --git a/vm/benchmarks/run-gc-verify.sh b/vm/benchmarks/run-gc-verify.sh index 5c9600ced85..d6324e7012e 100755 --- a/vm/benchmarks/run-gc-verify.sh +++ b/vm/benchmarks/run-gc-verify.sh @@ -122,4 +122,50 @@ else fail=1 fi +# Third self-test, for java.lang.ref. The referent is the ONE reference field the +# generated mark functions deliberately do not hand to gcMarkObject -- that +# suppression is what makes the edge weak -- and for a while it therefore bypassed +# the verifier completely: a live Reference holding a pointer into reclaimed memory +# passed with violations=0, which is the single defect this collector work most +# needs caught. cn1GcDiscoverReference now routes it to cn1GcVerifyChild, and this +# proves that routing has teeth rather than assuming it. +# +# refnoclear leaves a dead referent in its field instead of clearing it. Note the +# obvious-looking fault is the wrong one: clearing MORE references than liveness +# warrants only produces extra nulls, which are safe, and an attempt at that +# reported violations=0 for exactly that reason. The dangling direction is +# clearing LESS. +printf '%-16s ' "self-test3" +# REMOVE FIRST, THEN BUILD, AND CHECK THE STATUS. Three things are needed and only the +# third is obvious. Rebuilding unconditionally is not enough on its own: translate-and-build +# replaces its output only after the final compiler run succeeds, so a failed rebuild leaves +# the PREVIOUS binary in place. Nor is `|| true` harmless: it discards the status, and the +# -x test below then accepts that stale executable. Either way the self-test runs old code +# and reports green -- the "gate that cannot fail" problem this self-test exists to prevent, +# reintroduced in how the self-test is built. +rm -f ./target/bin/RefPolicy-verify +if ! ./translate-and-build.sh RefPolicy target/bin/RefPolicy-verify -DCN1_GC_VERIFY \ + > target/bin/RefPolicy-selftest-build.log 2>&1; then + echo "BROKEN -- could not build RefPolicy for the reference self-test" + tail -25 target/bin/RefPolicy-selftest-build.log + fail=1 +fi +if [ ! -x ./target/bin/RefPolicy-verify ]; then + echo "BROKEN -- could not build RefPolicy for the reference self-test" + fail=1 +else + rcOut="$(CN1_GC_FAULT=refnoclear ./target/bin/RefPolicy-verify 128 8192 1500 24 2>&1)" || true + if printf '%s' "$rcOut" | grep -q 'DANGLING REFERENCE'; then + echo "detected the injected dangling referent ($(printf '%s' "$rcOut" | grep -c 'DANGLING REFERENCE') reports)" + elif ! printf '%s' "$rcOut" | grep -q 'GC-VERIFY. SUMMARY'; then + echo "BROKEN -- faulted run died before the verifier summary" + printf '%s\n' "$rcOut" | tail -20 + fail=1 + else + echo "BROKEN -- an uncleared dead referent was NOT reported; the verifier cannot see referents" + printf '%s\n' "$rcOut" | tail -5 + fail=1 + fi +fi + [ "$fail" -eq 0 ] && echo "GC-VERIFY GREEN" || { echo "GC-VERIFY FAILED"; exit 1; } diff --git a/vm/benchmarks/src/com/bench/RefPolicy.java b/vm/benchmarks/src/com/bench/RefPolicy.java new file mode 100644 index 00000000000..a8a7f3eac70 --- /dev/null +++ b/vm/benchmarks/src/com/bench/RefPolicy.java @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2012, Codename One 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. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.bench; + +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; + +/** + * The driver behind ParparVM's java.lang.ref support: one half asserts that + * references behave, the other half is the workload the retention policy is + * chosen from. + * + *

Why both halves are here

+ * + *

They fail in opposite directions, and a driver that only had one of them + * would pass while the VM was badly wrong. A reference that is never cleared is + * the bug this feature replaced -- ParparVM traced the referent like any other + * field, so every cache built on {@code Display.createSoftWeakRef} pinned its + * contents for the life of the process. A reference cleared too eagerly is the + * bug BEFORE that one -- the constructor dropped its argument, so {@code get()} + * always answered null and the same caches could never hit. Phase A pins the + * first end, phase B the second: a cache with a zero hit rate is a cache that is + * being emptied faster than it is filled.

+ * + *

Reading the output

+ * + *
+ * WEAK_LIVE_KEPT=n/n        phase A: referents still strongly held; must be all
+ * WEAK_DEAD_CLEARED=n/m     phase A: unreachable referents the collector cleared
+ * HITS / MISSES / HIT_RATE  phase B: what the cache actually bought
+ * FINAL_FOOTPRINT_KB        phase B: what it cost
+ * CHECKSUM                  policy invariant -- see below
+ * 
+ * + *

HIT_RATE and FINAL_FOOTPRINT_KB are the pair the policy is chosen on, and + * NEITHER MEANS ANYTHING ALONE. A policy that never clears wins the hit rate by + * keeping everything, and a policy that clears everything wins the footprint by + * keeping nothing; the question ranking has to answer is whether it holds a + * higher hit rate than the clear-everything arm at the same footprint.

+ * + *

CHECKSUM is deliberately independent of the policy: a rebuilt payload is + * byte-for-byte what it replaced, and the checksum accumulates what was READ + * rather than what was rebuilt. So it is a parity check across arms (and against + * a host JVM) that a policy change cannot legitimately move, while the hit rate + * is free to move as much as it likes.

+ * + *

The trap this driver exists inside

+ * + *

ParparVM scans native C stacks conservatively, so a machine word left behind + * by a returned frame keeps whatever it points at marked. A driver that drops + * references and then sleeps pins them with its own dead frames and reports every + * referent as retained -- the measurement comes back green having measured + * nothing. {@link #scrub(int)} overwrites that region on purpose before any + * assertion about collection; every phase-A step goes through + * {@link #quiesce()}.

+ */ +public class RefPolicy { + + // Phase B's shape, from argv: keys, payload bytes, accesses, churn per access. + // + // SIZE THE CACHE AGAINST THE BUDGET, not against convenience. The defaults are 2048 + // keys of 32KB, so a fully retained cache is 64MB -- most of the ceiling the A/B runs + // under (CN1_SIMULATE_PROC_MEMORY_LIMIT). That is the only regime in which the + // retention policy is the variable: an earlier 512x4KB cache was 2MB against a 96MB + // ceiling, every policy retained all of it, and the run reported a 99.9% hit rate for + // all three arms while measuring nothing but the collector's reaction to the ceiling. + static int keys = 2048; + /** Payload bytes. Well over CN1_BIBOP_MAX_OBJECT (512), so these take the legacy path. */ + static int payload = 32768; + static int accesses = 400000; + /** Objects allocated per access, to drive the collector rather than wait for it. */ + static int churnPerAccess = 24; + /** Referents phase A drops. */ + static final int WEAK_SAMPLES = 256; + + static final int SCRUB_DEPTH = 400; + static long scrubSink; + + /** Cache of SoftReference tokens; index is the key. */ + static Object[] cache; + /** Somewhere for churn to land so the optimizer cannot delete it. */ + static Object[] churnSink = new Object[8]; + + static long hits; + static long misses; + static long checksum; + + /** Aliases per referent in the alias phase. */ + static final int ALIASES = 4; + + public static void main(String[] args) throws Exception { + if (args != null) { + if (args.length > 0) { keys = Integer.parseInt(args[0]); } + if (args.length > 1) { payload = Integer.parseInt(args[1]); } + if (args.length > 2) { accesses = Integer.parseInt(args[2]); } + if (args.length > 3) { churnPerAccess = Integer.parseInt(args[3]); } + } + cache = new Object[keys]; + System.out.println("CONFIG keys=" + keys + " payloadBytes=" + payload + + " accesses=" + accesses + " churnPerAccess=" + churnPerAccess + + " cacheBytes=" + ((long) keys * payload)); + weakPhase(); + aliasPhase(); + cachePhase(); + System.out.println("RESULT=" + checksum); + } + + // ---------------------------------------------------------------- phase A + + /** + * Both ends of the weak-reference contract, in one pass over one array. + * + *

Half the referents stay strongly reachable through {@code live} and must + * still be answered; the other half are dropped and must eventually stop + * being answered. Asserting only the second half would pass on a VM that + * cleared every reference unconditionally.

+ * + *

"Eventually" is two collections, not one, and that is not slop: the + * sweep's grace rule keeps anything allocated since the last sweep, so a + * referent is never cleared in the cycle it dies.

+ */ + private static void weakPhase() throws Exception { + Object[] live = new Object[WEAK_SAMPLES]; + Object[] refsLive = new Object[WEAK_SAMPLES]; + Object[] refsDead = new Object[WEAK_SAMPLES]; + + for (int i = 0; i < WEAK_SAMPLES; i++) { + byte[] kept = build(i); + live[i] = kept; + refsLive[i] = new WeakReference(kept); + + byte[] doomed = build(i + WEAK_SAMPLES); + refsDead[i] = new WeakReference(doomed); + // The only strong path to `doomed` ends here. Nothing else in this frame + // may keep it: no array slot, no local that outlives the iteration. + } + + quiesce(); + quiesce(); + + int liveKept = 0; + for (int i = 0; i < WEAK_SAMPLES; i++) { + if (((Reference) refsLive[i]).get() != null) { + liveKept++; + } + } + int deadCleared = 0; + for (int i = 0; i < WEAK_SAMPLES; i++) { + if (((Reference) refsDead[i]).get() == null) { + deadCleared++; + } + } + // `live` is read after the counting loop so it cannot be optimized away + // before it, which would turn the retention half into a tautology. + for (int i = 0; i < WEAK_SAMPLES; i++) { + checksum += ((byte[]) live[i])[0]; + } + + System.out.println("WEAK_LIVE_KEPT=" + liveKept + "/" + WEAK_SAMPLES); + // AN ASSERTION, not a printed number. A referent still reachable through `live` + // must never be cleared, and until this exited nonzero the driver reported a + // reduced count and finished successfully: the checksum reads live[] directly so + // it does not move, and run-gc-verify cannot see it either, because a field cleared + // too early is heap-SAFE -- null dangles nothing. Both advertised validation paths + // could therefore stay green while WeakReference was broken in the direction that + // silently empties every cache built on it. + if (liveKept != WEAK_SAMPLES) { + System.out.println("FAIL: " + (WEAK_SAMPLES - liveKept) + + " strongly reachable referent(s) were cleared"); + System.exit(2); + } + System.out.println("WEAK_DEAD_CLEARED=" + deadCleared + "/" + WEAK_SAMPLES); + } + + /** Set by the racing reader so the collector's clear pass sees fresh touch stamps. */ + static volatile boolean aliasRacing; + static volatile Object aliasSink; + static Object[] aliasRefs; + + /** + * Several references over ONE referent must agree, INCLUDING while a mutator is + * reading them. + * + *

The contract is that all references to a weakly reachable object are cleared + * atomically -- not one at a time -- and a collector that clears them entry by entry + * cannot honour it: clear the first alias, let a mutator's {@code get()} on the second + * stamp it as recently read before the loop arrives there, and the second is kept. One + * alias then answers null while another answers the object.

+ * + *

For a cache that is a spurious miss. For the callers that use a reference as a + * LIFETIME ORACLE -- reading a null {@code get()} as proof the referent died, and + * releasing something on that basis -- it is a false death report on one alias while + * the object is demonstrably alive through another.

+ * + *

This is NOT a self-test for the split, and the distinction matters. The + * split needs a {@code get()} to land between the clear pass reaching one alias of a + * group and reaching another, and that window is microseconds wide. Measured here: + * with {@code -DCN1_REF_NO_ALIAS_ATOMICITY} restoring the single-loop form that has + * the bug, three runs reported {@code ALIAS_SPLIT=0/256} -- the same as the fixed + * build -- while both collected 255 of 256 groups. So the phase exercises the path + * with real concurrent readers and asserts a real invariant, but it cannot be cited + * as evidence that the invariant holds: it has never been seen to fail. Do not read + * a green {@code ALIAS_SPLIT} as proof.

+ * + *

Two earlier versions were worse and are worth not rebuilding. Reading the + * aliases only after quiescing asserted something that could not fail at all, since + * with no {@code get()} in flight every alias carries the same stamp. Hammering the + * FIRST alias without pausing was hollow in the other direction: it kept every + * referent marked, so nothing was ever condemned and {@code ALIAS_CLEARED_GROUPS} was + * 0/256 -- a test in which the thing being tested never happens. Check that number + * before trusting the one above it.

+ * + *

The assertion is about AGREEMENT, not about collection: all cleared and all kept + * both pass, split does not. Whether a group is collected at all depends on the + * conservative root scan and on whether the reader happened to be holding it.

+ */ + private static void aliasPhase() throws Exception { + final int groups = 256; + aliasRefs = new Object[groups * ALIASES]; + for (int g = 0; g < groups; g++) { + byte[] doomed = build(g + 4096); + for (int a = 0; a < ALIASES; a++) { + aliasRefs[g * ALIASES + a] = new WeakReference(doomed); + } + // `doomed` dies with this iteration; only the aliases above refer to it. + } + + aliasRacing = true; + Thread reader = new Thread(new Runnable() { + public void run() { + // The LAST alias of each group, and only in bursts. + // + // Last, because the split needs the read to land after the pass has + // already cleared the group's earlier aliases -- a read that lands on the + // first entry finds the group not yet condemned and changes nothing. + // + // In bursts, because a reader that never pauses keeps every referent + // marked, so nothing is ever condemned and no split is possible. The pause + // lets a group become collectable between bursts. + while (aliasRacing) { + // ONE GROUP IN FOUR. Touching every group keeps every group alive: + // the stamp now survives until the end of the clear pass, so any read + // anywhere in a cycle makes sub-pass A mark that referent, and with + // one alias of each group read per burst nothing is ever condemned. + // That was measured -- ALIAS_CLEARED_GROUPS=0/256 -- and it is the + // vacuum this phase exists to detect, not to fall into. Leaving three + // groups in four untouched keeps the collection half honest while the + // touched quarter still exercises the interleaving. + for (int g = 0; g < groups; g += 4) { + aliasSink = ((Reference) aliasRefs[g * ALIASES + ALIASES - 1]).get(); + } + aliasSink = null; + try { + Thread.sleep(1); + } catch (InterruptedException e) { + return; + } + } + } + }); + reader.start(); + + for (int i = 0; i < 12; i++) { + quiesce(); + } + + aliasRacing = false; + reader.join(); + scrub(SCRUB_DEPTH); + + int split = 0; + int clearedGroups = 0; + for (int g = 0; g < groups; g++) { + int cleared = 0; + for (int a = 0; a < ALIASES; a++) { + if (((Reference) aliasRefs[g * ALIASES + a]).get() == null) { + cleared++; + } + } + if (cleared == ALIASES) { + clearedGroups++; + } else if (cleared != 0) { + split++; + } + } + System.out.println("ALIAS_SPLIT=" + split + "/" + groups); + System.out.println("ALIAS_CLEARED_GROUPS=" + clearedGroups + "/" + groups); + // A SPLIT IS A FAILURE, not a statistic: all cleared and all kept both satisfy the + // contract, one alias cleared beside a live one does not. + // + // Do NOT read this exit as making the phase a detector for that violation. The + // class javadoc records the measurement: with -DCN1_REF_NO_ALIAS_ATOMICITY putting + // the single-loop bug back, three runs still reported ALIAS_SPLIT=0/256, because + // catching it needs a get() inside a window microseconds wide. The assertion + // costs nothing and is right to make, but a green run remains evidence of nothing, + // and the ablation above is what actually has to be re-run to test this path. + if (split != 0) { + System.out.println("FAIL: " + split + " group(s) left partly cleared"); + System.exit(2); + } + } + + // ---------------------------------------------------------------- phase B + + /** + * A cache of rebuildable payloads under a skewed access distribution, which + * is the shape every real caller of this feature has: a decoded bitmap behind + * an EncodedImage, the int[] behind Image.getRGB, a rasterized gradient. + * + *

Skewed rather than uniform on purpose. Under a uniform distribution + * there is no such thing as a cold entry, so every retention policy that + * keeps the same NUMBER of entries scores the same and ranking cannot show a + * difference even if it has one. The skew is what makes "which entries" a + * question with an answer.

+ */ + private static void cachePhase() { + long seed = 0x2545F4914F6CDD1DL; + for (int i = 0; i < accesses; i++) { + seed = seed * 6364136223846793005L + 1442695040888963407L; + int r = (int) ((seed >>> 33) % keys); + // Squaring a uniform draw concentrates it near zero: a few hot keys, a + // long cold tail. + int key = (int) (((long) r * r) / keys); + + byte[] buf = null; + Object token = cache[key]; + if (token != null) { + buf = (byte[]) ((Reference) token).get(); + } + if (buf == null) { + misses++; + buf = build(key); + cache[key] = new SoftReference(buf); + } else { + hits++; + } + // Accumulate what was READ. A rebuild reproduces the same bytes, so this + // is identical across policies while the hit rate is not. + checksum += buf[0] + buf[payload - 1]; + + for (int c = 0; c < churnPerAccess; c++) { + churnSink[c & 7] = new byte[64]; + } + churnSink[0] = null; + } + + long total = hits + misses; + System.out.println("HITS=" + hits); + System.out.println("MISSES=" + misses); + System.out.println("HIT_RATE_PPM=" + (total == 0 ? 0 : (hits * 1000000L) / total)); + System.out.println("FINAL_FOOTPRINT_KB=" + footprintKb()); + } + + // ---------------------------------------------------------------- helpers + + /** Deterministic content, so a rebuild is byte-for-byte what it replaced. */ + private static byte[] build(int key) { + byte[] b = new byte[payload]; + b[0] = (byte) key; + b[payload - 1] = (byte) (key * 31); + return b; + } + + /** One full collection with the driver's own stack overwritten first. */ + private static void quiesce() throws Exception { + scrub(SCRUB_DEPTH); + System.gc(); + Thread.sleep(250); + } + + /** + * Overwrites the native C stack the loop above ran on. See the class comment: + * without this the conservative root scan keeps the dropped referents marked + * and phase A reports zero collections while claiming success. + */ + private static long scrub(int depth) { + long a = depth * 0x5DEECE66DL; + long b = a ^ 0x1234567890ABCDEFL; + long c = b + 0x0F0F0F0F0F0F0F0FL; + long d = c ^ 0x7FFFFFFFFFFFFFFFL; + if (depth > 0) { + a += scrub(depth - 1); + } + scrubSink = a ^ b ^ c ^ d; + return scrubSink; + } + + /** + * Physical footprint, read in process. On Apple platforms totalMemory() is + * physical RAM and freeMemory() is RAM minus phys_footprint, which is the + * number Apple's own limits are enforced against -- and the reason not to use + * RSS, which counts shared clean pages and moves with whatever else the + * machine is doing. + */ + private static long footprintKb() { + Runtime r = Runtime.getRuntime(); + return (r.totalMemory() - r.freeMemory()) / 1024; + } +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptRuntimeSemanticsTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptRuntimeSemanticsTest.java index fe097c97754..368591c0dec 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptRuntimeSemanticsTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/JavascriptRuntimeSemanticsTest.java @@ -271,6 +271,12 @@ void weakReferenceHoldsTheReferentItWasConstructedWith(CompilerHelper.CompilerCo // // This runs the real vm/JavaAPI class through the translator and the // worker runtime, which is the only place the bug was observable. + // + // Every referent in the fixture stays strongly reachable, so this pins the + // reference's own behaviour and is unaffected by the collector now clearing + // referents. The opposite end -- that an unreachable referent stops being + // answered -- is RefPolicy in vm/benchmarks, which needs a driver that scrubs + // its native stack and therefore cannot live here. WorkerRunResult result = translateAndRunFixture(config, "JsWeakReferenceApp.java", "JsWeakReferenceApp"); assertEquals(511, result.result, diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/JsWeakReferenceApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/JsWeakReferenceApp.java index 37bf34aab01..f58b661b2c2 100644 --- a/vm/tests/src/test/resources/com/codename1/tools/translator/JsWeakReferenceApp.java +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/JsWeakReferenceApp.java @@ -33,10 +33,18 @@ * built on CodenameOneImplementation.createSoftWeakRef -- the EncodedImage * decode cache above all -- into a cache that can never hit. * - * The collector has no weak roots, so a reference here holds its referent until - * it is cleared by hand. That is the pessimistic half of the contract and is - * what these assertions pin down; what they exist to catch is the other half - * going missing again. + * Every referent below is strongly reachable through a local for as long as it + * is asserted on, so these assertions are about the REFERENCE, not about the + * collector: a reference must answer the object it was handed, distinguish its + * referent from another reference's, and empty only when cleared. They hold + * whether or not weak roots are implemented, which is the point -- the + * collector clearing referents is covered by RefPolicy in vm/benchmarks, where + * the referent is deliberately dropped and the driver scrubs its own native + * stack first. + * + * Note this fixture runs on the JavaScript backend, whose gcMarkSweep is a + * no-op: the host JS GC collects, and ParparVM's reference clearing (which + * lives in the C collector) does not run here at all. */ public class JsWeakReferenceApp { static int result; @@ -57,7 +65,7 @@ public static void main(String[] args) throws Exception { mask |= 2; } - // clear() is the only thing that empties a reference on this VM. + // clear() empties a reference by hand, independently of the collector. ref.clear(); if (ref.get() == null) { mask |= 4;