Epsilon and Shenandoah Internals
Purpose
Use Epsilon to turn an argument about allocation into a measurement, and reason about Shenandoah from its actual mechanism — a conditional load barrier whose slow path copies objects in the application thread, a concurrent cycle with a finite time budget, a pacer that stalls allocating threads before anything appears as a pause, and a generational mode that is product but not default. Both are misused in the same way: Epsilon as a "GC-free performance mode", Shenandoah as a collector whose only knob is heap size.
The failure this prevents is the conclusion drawn from the wrong configuration. A Shenandoah
throughput comparison that never named its mode is underspecified; JDK 25 defaults to satb,
but omission from a report does not prove what ran. A service left on Epsilon because it "went faster in
the benchmark" is an out-of-memory error with a countdown on it.
Workflow
- Decide what Epsilon is being asked to prove. Isolating a benchmark from collection, verifying an allocation-free path, or making hidden allocation visible are three different experiments with three different heap sizes.
- Size Epsilon from the arithmetic, not by feel.
Estimate remaining time from usable headroom divided by total consumption rate. Budget
cumulative startup/warm-up allocation; finite survival does not prove zero allocation. See
references/epsilon-as-an-instrument.md. - Pair Epsilon with allocation evidence and, when useful, a heap dump on OOM. Because Epsilon never reclaims, the dump contains all still represented allocations—not just objects a real collector would retain—and lacks allocation stacks. Use it for class/graph clues, then attribute sites with JFR/async-profiler. Reserve disk/native headroom for dump creation. For an allocation-free claim, read the post-warm-up slope rather than the mere OOM.
- For Shenandoah, confirm the build and the effective mode before measuring anything.
java -XX:+UseShenandoahGC -version(Oracle JDK builds have no Shenandoah), then-Xlog:gc+initforMode:andHeuristics:, orjcmd <pid> VM.flags -all | grep -E "ShenandoahGCMode|ShenandoahGCHeuristics". Product is not default. - Check the time constraint and the capacity constraint separately. Time:
(InitFreeThreshold − MinFreeThreshold)% × soft max / allocation rateis a rough learning headroom model, not a guaranteed failure deadline. Inspect actual triggers. Capacity: the configuredMax Evacuationbudget and actualavailablein thegc+ergolines. A heap can satisfy one and violate the other. - Look for pacing before looking for pauses.
-Xlog:gc+stats→Allocation pacing accruedper thread. Correlate affected requests; absent pauses alone do not identify pacing. - Classify a fallback before reacting to it. The degeneration point (
Mark,Evacuation,Update Refs,Roots,Outside of Cycle) andGood/Bad progressname the phase and outcome, not a unique root cause. Seereferences/shenandoah-log-and-troubleshooting.md. - Investigate barrier cost alongside concurrent work with a CPU profile: the slow path is the
ShenandoahRuntime::load_reference_barrier_*frames in application threads; the fast path is inlined. A collector comparison changes more than barriers and cannot isolate it by subtraction.
Rules
-XX:+UnlockExperimentalVMOptionsis always required for Epsilon on JDK 25, and must precede-XX:+UseEpsilonGC. Epsilon was never promoted to product — unlike ZGC and Shenandoah (both product in JDK 15, JEP 377 and JEP 379). Any document claiming the flag stopped being necessary describes an event that never happened.- Epsilon exits the process on OOM. It sets
ExitOnOutOfMemoryError=trueby default: the JVM printsTerminating due to java.lang.OutOfMemoryErrorand exits with status 3; nocatch,finallyor shutdown hook runs (verified on 25.0.3). Pass-XX:-ExitOnOutOfMemoryErrorwhen something in-process must observe the error. The heap dump is written before the exit. - Epsilon needs a bounded whole-process allocation budget including background work, or recycling before conservative exhaustion. An allocation-free hot path alone is insufficient; otherwise it is an OOM on a timer.
- The Shenandoah barrier is the Load Reference Barrier: a load barrier, on reference
loads. Since JDK 13 (JDK-8221766) it is conditional — a thread-local
gc_statetest, then a collection-set test, then a slow path that resolves or copies the object in the application thread and heals the slot. Writes carry the SATB pre-write barrier during marking and, in generational mode, a card mark. Calling it a read barrier finds the wrong symbol; calling it unconditional overstates its idle cost and misses where its real cost lands (mutator evacuation duringConcurrent evacuation). - Shenandoah has no per-object forwarding word since JDK 13 (JDK-8224584): forwarding
lives in the mark word. Verified on 25.0.3:
java.lang.Objectis 16 bytes under Shenandoah and G1 alike, 8 with-XX:+UseCompactObjectHeaders. A footprint model charging Shenandoah 8 bytes per object is a JDK 12 model. Compressed oops work; ZGC's do not. - Generational Shenandoah is product in JDK 25 (JEP 521), experimental in JDK 24 (JEP
404), and not the default:
-XX:+UseShenandoahGCalone runssatb(verified). JEP 535 (JDK-8379682) targets JDK 28 for the default change andsatbdeprecation (checked 2026-09-05); targeted is not delivered. State the effective mode from the runtime; never infer it from a future proposal. - Generational mode adds a post-write barrier feeding a card-table remembered set (512-byte cards), on top of the LRB. The LRB cannot serve that purpose: the old-to-young relation can only be captured when the reference is written.
ShenandoahInitFreeThreshold(70),ShenandoahMinFreeThreshold(10),ShenandoahLearningSteps(5) and every other threshold are experimental flags: without-XX:+UnlockExperimentalVMOptionsbefore them the JVM refuses to start (verified).InitFreeThresholdgoverns the learning phase only — at start-up and again after every degenerated or full GC;MinFreeThresholdis the floor in every phase.- During learning/relearning, raising
InitFreeThresholdstarts earlier and grows the simple headroom termIFT − MFT; it can also spend more concurrent CPU and is not the adaptive steady-state control. Change it only when logs show learning-phase/spike degeneration, then validate pacing, CPU, cycle interval and fallback rate. Lowering it reduces that headroom. - The pacer is on by default (
ShenandoahPacing=true) and stalls allocating threads againstShenandoahPacingMaxDelay(10 ms) per episode; scheduling can overshoot and a request can encounter several episodes. It shows up nowhere in-Xlog:gc; only-Xlog:gc+statsreports it. Verified: 51% of a thread's time paced with zero degenerated cycles in the log. ShenandoahGCMode=passiveandShenandoahGCHeuristics=aggressiveare diagnostic and need-XX:+UnlockDiagnosticVMOptions(verified).passivedisables collector barriers and concurrent heuristic cycles; allocation failures and explicit requests can cause STW degenerated/full collection. It does evacuate and compact. Never a production setting.Degenerated GCis notFull GC.(Mark),(Evacuation),(Update Refs)resume the running cycle in STW from that phase;(Outside of Cycle)runs a whole cycle STW;Bad progressupgrades to full GC (immediately insatb, after two in generational), as do three back-to-back degenerations (ShenandoahFullGCThreshold). Recurring degenerated GC can reflect insufficient headroom; recurring full GC needs cause/flag/capacity evidence. No threshold creates space for an oversized live set.System.gc()normally requests a concurrent cycle insatb/generational mode withExplicitGCInvokesConcurrent=true.DisableExplicitGC, overrides and passive mode change this; inspect effective flags and logged causes.- Enlarging the heap raises
C_maxlinearly but does not reduce marking work per cycle: single-generation Shenandoah marks every live object, young or old, every cycle. For high young-allocation workloads, the generational mode attacks the cause; more heap only buys time. - Treat every barrier symbol name as a starting point to confirm against the build in use.
ShenandoahBarrierSet::need_load_reference_barrieris a compile-time predicate and never appears on a mutator stack; the runtime frames areShenandoahRuntime::*.
Production and security constraints
- Epsilon is an experiment with a calculated memory and time envelope. Run it in an isolated canary/job with container and native-memory headroom; an automatic OOM dump can prolong failure, consume disk and contain secrets.
- GC/JFR logs and dumps are production data. Restrict attach/read access, encrypt storage, minimize retention and sanitize thread/object fields before sharing.
- A collector comparison must pin JDK update/vendor/build, collector mode, heap/container limits, load and warm-up, and report application CPU/throughput/tail latency plus pacing/fallbacks.
References
- Epsilon as an instrument — the time-to-OOM arithmetic in both directions, the exit-on-OOM default and lazy commit, the four legitimate uses with the heap size each implies, the two-phase allocation-free test, the verified log format, and the OOM-plus-heap-dump procedure. Read before running Epsilon, and when sizing a heap for a benchmark or a short-lived process.
- Shenandoah internals — the LRB as it is on JDK 25 with the frames that are and are not the barrier, the cost shape against ZGC, the phase sequence, generational mode and its remembered set, the trigger order of the adaptive heuristic, the budget formula worked through, the pacer, every mode and heuristic with its unlock requirement, the flag table with kinds, and the fallback matrix. Read when choosing a mode, a heuristic or a threshold, or when reasoning about barrier overhead.
- Log and troubleshooting — the JDK 25 log lines for start-up, triggers, a single-generation cycle, a generational cycle, the fallbacks and the pacing report; the jcmd and JFR surfaces; the symptom table; and the source file index. Read when a Shenandoah log shows a fallback, when latency rose without a pause, or before writing a parser or an incident write-up.