ZGC and Shenandoah
Purpose
Run a concurrent collector with its real cost budgeted. Short pauses are not zero cost: work
moves into concurrent phases, barriers and metadata, with collector/application CPU and
memory-bandwidth contention that a pause histogram does not quantify. Barrier shape and when
its slow path runs differ by collector, generation and cycle state.
The failure this prevents is the migration that meets its p99 target and is then reverted,
because the CPU and heap headroom the concurrent phases need were never budgeted, or because
the pod had no spare cores to pay for them in the first place.
Workflow
Use JDK 25 HotSpot as the examples' baseline, then inspect the deployed vendor/build, OS/architecture,
container settings and effective flags. Collector inclusion is a build choice; a product JEP does
not guarantee every distribution supplies that collector. Smoke-test the actual binary before
planning a workload experiment. Do not upgrade the runtime merely to match this guide.
- Separate the three cost axes before reading any number. STW pause, concurrent work
(CPU while the application runs), and per-access barrier overhead. Conflating them is the
most common source of a wrong conclusion about these collectors.
- Check effective CPU quota, throttling and topology in the target environment. Small
quotas increase contention, but no core-count threshold selects a collector. Compare
throughput and tail latency under the actual quota and overload policy.
- Declare the mode explicitly. ZGC has exactly one mode since JDK 24 and needs only
-XX:+UseZGC. On the JDK 25 baseline Shenandoah defaults to single-generation; generational is opt-in via
-XX:ShenandoahGCMode=generational. Confirm what is actually active with startup
gc+init logs and jcmd <pid> VM.flags -all; plain VM.flags can omit defaults.
- Audit every carried flag. G1-specific flags may remain accepted yet be inert under
another collector, while global flags can still apply. Prove effective relevance from
startup logs/flag metadata and remove only with a before/after launch and workload check.
- Capture logs per cycle/generation (where applicable) and phase with
-Xlog:gc*,gc+phases=debug, and read
pauses separately from concurrent phase durations. See
references/reading-concurrent-gc-logs.md.
- Treat allocation stalls/failures as capacity evidence, then classify why. Heap/live-set
headroom, allocation spikes, concurrent-worker CPU, cycle-start prediction, fragmentation,
large allocations and collector fallback paths imply different remedies.
- Budget CPU, memory bandwidth and heap/native headroom before migration. Verify cgroup
charge/RSS, not merely
-Xmx, and preserve room for allocation while relocation completes.
Rules
- Activate ZGC with
-XX:+UseZGC alone on JDK 24+. -XX:+ZGenerational is obsolete since JDK 24 —
accepted with a warning and no effect on the tested JDK 25 build, because the only mode is already
generational. Prescribing it looks like configuration and changes nothing.
- ZGC is generational by definition on JDK 24+. JEP 474 made it the default in JDK 23;
JEP 490 deleted the non-generational code in JDK 24. There is no mode to turn off.
- Shenandoah generational is product in JDK 25 (JEP 521, experimental in JDK 24 under JEP 404) but is not the default.
-XX:+UseShenandoahGC on its own still selects
single-generation. "Product" is not "default".
- Require the effective Shenandoah mode for a comparison. On JDK 25, omission can compare
single-generation Shenandoah with generational ZGC. That can answer a defaults comparison,
but does not isolate collector implementation from generation policy; state the question.
- Shenandoah's load-reference barrier resolves forwarded references; its fast/slow paths and
barrier set depend on mode and GC state. ZGC uses colored pointers and load/store barriers.
Avoid universal branch/cycle claims: inspect generated code/profiles on the target build.
- Classic Shenandoah layouts reserve forwarding metadata per object in the heap. Quantify the
effective heap/live-set cost from collector accounting and the target object mix; JOL's
ordinary shallow instance size need not include collector-private allocation overhead.
- ZGC generational adds a store barrier on top of the load barrier, to maintain
per-page remembered sets for old→young references. That is real extra per-access cost
traded for young-allocation throughput.
- Do not size a ZGC container from one
ps/top RSS sample or from folklore about legacy
heap multi-mapping. Multi-mapping history is not the same change as JEP 490's JDK 24 removal
of non-generational ZGC. Reconcile target-build RSS/PSS, cgroup memory.current, heap
committed/used and native domains over time.
- Prefer ergonomics first for ZGC, then tune only a measured constraint. Heap/soft max,
ConcGCThreads, CPU quota and allocation spikes interact; a copied knob can trade mutator
CPU for fewer stalls or merely hide a capacity defect.
- Use an arrival model that represents production and correct coordinated omission when
relevant. Report throughput, offered/achieved load, CPU throttling, allocation rate,
pause/stall distributions and p50/p99/p99.9/max. A JMH comparison can expose workload cost
but cannot isolate “barrier overhead” merely by changing collectors.
Production acceptance
- Exercise steady state, burst, live-set growth, large allocation, redeploy and CPU-throttle
scenarios. Set acceptable pause/stall/pacing and fallback behavior from the SLO; Shenandoah
pacing can be normal allocation control, not automatically a failed migration. Distinguish
pacing delay from degeneration/full fallback and inspect any consequential regression.
- Compare declared collector modes and effective flags on the same JDK build/quota; include
warm-up and confidence/repetition rather than a single run.
- Set rollback on SLO, achieved throughput, CPU throttling and memory headroom. Preserve GC,
safepoint and OS/cgroup evidence for every failed run.
Return the exact build/mode/flags, measured pause versus concurrent wall time and CPU evidence,
the supported bottleneck hypothesis, proposed adjustment, and validation/rollback criteria.
If measurements are missing, propose a bounded experiment rather than presenting a tuning fix.
References
- Flags, modes and version corrections — the live flag set
for each collector, the JEP timeline, the obsolete and removed options, and how to verify
the mode that is actually running. Read before changing a collector flag or auditing a
configuration carried over from an older JDK.
- Reading concurrent GC logs — capture commands,
the ZGC and Shenandoah log shapes, the allocation-stall signature, and where barrier cost
shows up in a profile. Read when diagnosing latency or throughput on a running concurrent
collector.
Authoritative sources: JEP 474,
JEP 490, JEP 521, and
Oracle JDK 25 ZGC guide.
1---2name: zgc-and-shenandoah3description: Operating ZGC and Shenandoah in production: concurrent relocation via coloured pointers and load barriers, the CPU the concurrent phases actually take, allocation stalls, and which flags still exist. Use when a service migrated to a concurrent collector and throughput dropped, when a GC log shows "Allocation Stall", when a pod of 1-2 CPUs runs ZGC or Shenandoah, when a config still carries -XX:+ZGenerational or G1 flags after the migration, when a ZGC-versus-Shenandoah comparison does not declare ShenandoahGCMode, or when RSS from ps/top is being used to size a ZGC container. Does not cover deciding whether GC is the bottleneck or which collector to pick (jvm-gc-tuning), the introductory collector model (gc-fundamentals), or collector source-level internals (zgc-generational-internals, epsilon-and-shenandoah-internals).4---56# ZGC and Shenandoah78## Purpose910Run a concurrent collector with its real cost budgeted. Short pauses are not zero cost: work11moves into concurrent phases, barriers and metadata, with collector/application CPU and12memory-bandwidth contention that a pause histogram does not quantify. Barrier shape and when13its slow path runs differ by collector, generation and cycle state.1415The failure this prevents is the migration that meets its p99 target and is then reverted,16because the CPU and heap headroom the concurrent phases need were never budgeted, or because17the pod had no spare cores to pay for them in the first place.1819## Workflow2021Use JDK 25 HotSpot as the examples' baseline, then inspect the deployed vendor/build, OS/architecture,22container settings and effective flags. Collector inclusion is a build choice; a product JEP does23not guarantee every distribution supplies that collector. Smoke-test the actual binary before24planning a workload experiment. Do not upgrade the runtime merely to match this guide.25261. **Separate the three cost axes before reading any number.** STW pause, concurrent work27 (CPU while the application runs), and per-access barrier overhead. Conflating them is the28 most common source of a wrong conclusion about these collectors.292. **Check effective CPU quota, throttling and topology in the target environment.** Small30 quotas increase contention, but no core-count threshold selects a collector. Compare31 throughput and tail latency under the actual quota and overload policy.323. **Declare the mode explicitly.** ZGC has exactly one mode since JDK 24 and needs only33 `-XX:+UseZGC`. On the JDK 25 baseline Shenandoah defaults to single-generation; generational is opt-in via34 `-XX:ShenandoahGCMode=generational`. Confirm what is actually active with startup35 `gc+init` logs and `jcmd <pid> VM.flags -all`; plain `VM.flags` can omit defaults.364. **Audit every carried flag.** G1-specific flags may remain accepted yet be inert under37 another collector, while global flags can still apply. Prove effective relevance from38 startup logs/flag metadata and remove only with a before/after launch and workload check.395. **Capture logs per cycle/generation (where applicable) and phase** with40 `-Xlog:gc*,gc+phases=debug`, and read41 pauses separately from concurrent phase durations. See42 `references/reading-concurrent-gc-logs.md`.436. **Treat allocation stalls/failures as capacity evidence, then classify why.** Heap/live-set44 headroom, allocation spikes, concurrent-worker CPU, cycle-start prediction, fragmentation,45 large allocations and collector fallback paths imply different remedies.467. **Budget CPU, memory bandwidth and heap/native headroom before migration.** Verify cgroup47 charge/RSS, not merely `-Xmx`, and preserve room for allocation while relocation completes.4849## Rules5051- Activate ZGC with `-XX:+UseZGC` alone on JDK 24+. `-XX:+ZGenerational` is obsolete since JDK 24 —52 accepted with a warning and no effect on the tested JDK 25 build, because the only mode is already53 generational. Prescribing it looks like configuration and changes nothing.54- ZGC is generational **by definition** on JDK 24+. JEP 474 made it the default in JDK 23;55 JEP 490 deleted the non-generational code in JDK 24. There is no mode to turn off.56- Shenandoah generational is _product_ in JDK 25 (JEP 521, experimental in JDK 24 under JEP 404) but is **not** the default. `-XX:+UseShenandoahGC` on its own still selects57 single-generation. "Product" is not "default".58- Require the effective Shenandoah mode for a comparison. On JDK 25, omission can compare59 single-generation Shenandoah with generational ZGC. That can answer a defaults comparison,60 but does not isolate collector implementation from generation policy; state the question.61- Shenandoah's load-reference barrier resolves forwarded references; its fast/slow paths and62 barrier set depend on mode and GC state. ZGC uses colored pointers and load/store barriers.63 Avoid universal branch/cycle claims: inspect generated code/profiles on the target build.64- Classic Shenandoah layouts reserve forwarding metadata per object in the heap. Quantify the65 effective heap/live-set cost from collector accounting and the target object mix; JOL's66 ordinary shallow instance size need not include collector-private allocation overhead.67- ZGC generational adds a **store barrier** on top of the load barrier, to maintain68 per-page remembered sets for old→young references. That is real extra per-access cost69 traded for young-allocation throughput.70- Do not size a ZGC container from one `ps`/`top` RSS sample or from folklore about legacy71 heap multi-mapping. Multi-mapping history is not the same change as JEP 490's JDK 24 removal72 of non-generational ZGC. Reconcile target-build RSS/PSS, cgroup `memory.current`, heap73 committed/used and native domains over time.74- Prefer ergonomics first for ZGC, then tune only a measured constraint. Heap/soft max,75 `ConcGCThreads`, CPU quota and allocation spikes interact; a copied knob can trade mutator76 CPU for fewer stalls or merely hide a capacity defect.77- Use an arrival model that represents production and correct coordinated omission when78 relevant. Report throughput, offered/achieved load, CPU throttling, allocation rate,79 pause/stall distributions and p50/p99/p99.9/max. A JMH comparison can expose workload cost80 but cannot isolate “barrier overhead” merely by changing collectors.8182## Production acceptance8384- Exercise steady state, burst, live-set growth, large allocation, redeploy and CPU-throttle85 scenarios. Set acceptable pause/stall/pacing and fallback behavior from the SLO; Shenandoah86 pacing can be normal allocation control, not automatically a failed migration. Distinguish87 pacing delay from degeneration/full fallback and inspect any consequential regression.88- Compare declared collector modes and effective flags on the same JDK build/quota; include89 warm-up and confidence/repetition rather than a single run.90- Set rollback on SLO, achieved throughput, CPU throttling and memory headroom. Preserve GC,91 safepoint and OS/cgroup evidence for every failed run.9293Return the exact build/mode/flags, measured pause versus concurrent wall time and CPU evidence,94the supported bottleneck hypothesis, proposed adjustment, and validation/rollback criteria.95If measurements are missing, propose a bounded experiment rather than presenting a tuning fix.9697## References9899- [Flags, modes and version corrections](references/flags-and-modes.md) — the live flag set100 for each collector, the JEP timeline, the obsolete and removed options, and how to verify101 the mode that is actually running. Read before changing a collector flag or auditing a102 configuration carried over from an older JDK.103- [Reading concurrent GC logs](references/reading-concurrent-gc-logs.md) — capture commands,104 the ZGC and Shenandoah log shapes, the allocation-stall signature, and where barrier cost105 shows up in a profile. Read when diagnosing latency or throughput on a running concurrent106 collector.107108Authoritative sources: [JEP 474](https://openjdk.org/jeps/474),109[JEP 490](https://openjdk.org/jeps/490), [JEP 521](https://openjdk.org/jeps/521), and110[Oracle JDK 25 ZGC guide](https://docs.oracle.com/en/java/javase/25/gctuning/z-garbage-collector.html).