JVM 25 GC Tuning Cheat Sheet: ZGC vs Shenandoah [2026]
Bottom Line
On JDK 25, ZGC is already generational and usually wants fewer knobs, while Generational Shenandoah is now productized and is a strong low-pause option on Shenandoah-enabled OpenJDK builds. Start with heap sizing, memory-page policy, and GC logging before touching deeper collector-specific knobs.
Key Takeaways
- ›JDK 25 removes non-generational ZGC; use -XX:+UseZGC only.
- ›Generational Shenandoah is a product feature in JDK 25; the old experimental unlock flag is no longer required.
- ›For ZGC, the primary tuning lever is usually -Xmx headroom, not dozens of collector flags.
- ›For Shenandoah, start with -Xms/-Xmx, -XX:+AlwaysPreTouch, large pages, and pacing visibility.
- ›Use -Xlog:gc* first; tune only after you can see stalls, pacing, degenerated cycles, and uncommit behavior.
Low-latency GC tuning got simpler and more opinionated by JDK 25. ZGC is now generational-only, while Generational Shenandoah graduated from experimental in JDK 25. That changes the startup flags you should keep, delete, and benchmark. This cheat sheet is built for fast operator decisions: compare collectors, filter commands live, jump with keyboard shortcuts, and copy working baselines without digging through JEPs and tuning guides.
- JDK 25: ZGC runs in generational mode by default; the old ZGenerational switch is not part of a clean config anymore.
- Generational Shenandoah no longer needs -XX:+UnlockExperimentalVMOptions on JDK 25.
- ZGC is designed for sub-millisecond pauses; Shenandoah typically trades a bit more pause time for strong low-latency behavior and flexible diagnostics.
- For both collectors, start with -Xlog:gc*, heap sizing, and page policy before trying niche knobs.
| Dimension | ZGC | Generational Shenandoah | Edge |
|---|---|---|---|
| JDK 25 status | Generational-only by default | Generational mode is a product feature | Tie |
| Simplest enable flag | -XX:+UseZGC | -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational | ZGC |
| Main first tuning lever | -Xmx headroom | -Xms/-Xmx plus pacing and page policy | ZGC |
| Low-pause design goal | Pause work kept under about 1 ms | Most workloads commonly land in very low single-digit ms to ~10 ms territory | ZGC |
| Large-page upside | Recommended on large heaps | Strongly recommended on large heaps | Tie |
| Failure-mode visibility | Clean logging, fewer manual heuristics | Pacing and degenerated/full-GC ladder is very explicit | Shenandoah |
| Operational friction | Lower | Higher, and depends on a Shenandoah-enabled build | ZGC |
Collector Snapshot
Bottom Line
ZGC is the cleaner default choice when you want the fewest tuning decisions on JDK 25. Reach for Generational Shenandoah when you already run Shenandoah-enabled OpenJDK builds and want its low-pause behavior, pacing controls, and diagnostic ladder.
What changed by JDK 25
- ZGC became generational by default in JDK 24, and the non-generational mode was removed in JDK 24; on JDK 25, treat -XX:+UseZGC as the normal entry point.
- Generational Shenandoah arrived as experimental in JDK 24 and became a product feature in JDK 25.
- The old -XX:+UnlockExperimentalVMOptions requirement for generational Shenandoah is no longer needed on JDK 25.
- ZGC is intentionally adaptive, so heap headroom matters more than collector micro-tuning in many deployments.
- Shenandoah exposes more operational levers around pacing, heuristics, and failure-mode analysis.
What to benchmark first
- p99 and p999 application latency under realistic allocation pressure.
- GC CPU share versus application CPU share.
- Allocation stall signals, especially if heap headroom is tight.
- RSS behavior when using uncommit-friendly settings and container limits.
- Throughput drift after enabling large pages or pre-touching memory.
When to Choose Each
Choose ZGC when:
- You want the smallest tuning surface and a clean JDK 25 low-latency baseline.
- Your workload is highly latency-sensitive and you can afford more heap headroom.
- You prefer adaptive behavior over collector-specific heuristics tuning.
- You want a straightforward operational story in containers and service fleets.
Choose Generational Shenandoah when:
- Your JDK distribution already ships Shenandoah and your team is comfortable operating it.
- You want explicit visibility into pacing, degenerated GC, and full-GC escalation.
- You need a second low-pause collector to compare against ZGC on the same hardware.
- You want more manual control over GC start behavior and related heuristics on Shenandoah-supported builds.
Practical selection rule
- Start with ZGC for greenfield low-latency services on JDK 25.
- Switch to or test Generational Shenandoah when you need Shenandoah-specific behavior, stronger pacing introspection, or better results on your exact workload.
- Do not pick either collector by folklore alone; benchmark with production-like object lifetimes, not toy microservices.
Command Cheat Sheet
Shortcuts: press / to focus search, Esc to clear, g c for this section, g z for ZGC, g s for Shenandoah.
| Shortcut | Action | Use case |
|---|---|---|
/ | Focus the live filter | Jump straight to a flag or config pattern |
Esc | Clear the filter | Reset the sheet quickly during incident review |
g c | Go to Command Cheat Sheet | Fast section navigation |
g z | Go to ZGC commands | Collector-specific jump |
g s | Go to Shenandoah commands | Collector-specific jump |
Enable and verify ZGC
java -XX:+UseZGC -Xlog:gc* -version
java -XX:+UseZGC -Xms4g -Xmx4g -Xlog:gc*:file=gc-zgc.log:time,uptime,level,tags -jar app.jar
- Use -XX:+UseZGC as the clean collector switch on JDK 25.
- Use -Xlog:gc* immediately so you can validate pause behavior and background GC cadence.
- If you are documenting launch lines for a team runbook, clean them up in the Code Formatter before they spread.
Enable and verify Generational Shenandoah
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -Xlog:gc* -version
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -Xms4g -Xmx4g -Xlog:gc*:file=gc-shen.log:time,uptime,level,tags -jar app.jar
- On JDK 25, skip -XX:+UnlockExperimentalVMOptions for generational mode.
- Make sure your JDK distribution actually includes Shenandoah support.
- Use the same heap and logging baseline you use for ZGC so the first benchmark is apples-to-apples.
Latency-first baselines
java -XX:+UseZGC -Xms8g -Xmx8g -XX:+AlwaysPreTouch -XX:+UseLargePages -jar app.jar
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -Xms8g -Xmx8g -XX:+AlwaysPreTouch -XX:+UseLargePages -jar app.jar
- Set -Xms equal to -Xmx when predictable latency matters more than elastic footprint.
- Use -XX:+AlwaysPreTouch to push page-fault cost into startup instead of request latency.
- Use -XX:+UseLargePages when your OS and privileges are configured correctly.
Memory footprint and OS interaction
java -XX:+UseZGC -Xms2g -Xmx8g -XX:+ZUncommit -XX:ZUncommitDelay=300 -jar app.jar
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -Xms2g -Xmx8g -XX:+UseTransparentHugePages -XX:+UseNUMA -jar app.jar
- For ZGC, -XX:+ZUncommit and -XX:ZUncommitDelay shape how aggressively unused heap returns to the OS.
- For Shenandoah, lower -Xms keeps room for uncommit behavior, while -XX:+UseNUMA and huge pages often help on bigger hosts.
- On Linux transparent huge pages, prefer a measured rollout rather than blanket enablement across every JVM.
Logging and diagnostics
java -XX:+UseZGC -Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags -jar app.jar
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -Xlog:gc+ergo,gc+stats,safepoint:file=gc.log:time,uptime,level,tags -jar app.jar
- Use gc+ergo when you need to understand why the collector started or delayed work.
- Use gc+stats on Shenandoah when you want end-of-run summaries for pacing, successful concurrent cycles, and degenerated/full GC counts.
- Add safepoint logging when GC pauses are not the only latency suspect.
Configuration Patterns
Pattern 1: Safe baseline for first benchmark
# ZGC
java -XX:+UseZGC -Xms4g -Xmx4g -Xlog:gc*:file=gc-zgc.log:time,uptime,level,tags -jar app.jar
# Generational Shenandoah
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -Xms4g -Xmx4g -Xlog:gc*:file=gc-shen.log:time,uptime,level,tags -jar app.jar
- Use this when you are choosing a collector, not squeezing the last percentile yet.
- Keep the same heap, load generator, and request mix for both runs.
Pattern 2: Tight latency budget
# ZGC
java -XX:+UseZGC -Xms8g -Xmx8g -XX:+AlwaysPreTouch -XX:+UseLargePages -Xlog:gc* -jar app.jar
# Generational Shenandoah
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -Xms8g -Xmx8g -XX:+AlwaysPreTouch -XX:+UseLargePages -Xlog:gc+ergo,gc+stats -jar app.jar
- Use equal -Xms and -Xmx to remove heap resizing noise.
- Expect longer startup and higher committed memory from pre-touching.
Pattern 3: Elastic memory footprint
# ZGC
java -XX:+UseZGC -Xms1g -Xmx8g -XX:+ZUncommit -XX:ZUncommitDelay=300 -Xlog:gc* -jar app.jar
# Generational Shenandoah
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -Xms1g -Xmx8g -Xlog:gc+ergo,gc+stats -jar app.jar
- Use this for services with large idle windows or bursty traffic.
- Watch for RSS oscillation and heap recommit costs after quiet periods.
Pattern 4: Container-friendly validation
- Give both collectors realistic CPU limits before judging them.
- Benchmark under the same cgroup memory ceiling you expect in production.
- Do not compare a roomy ZGC heap to a squeezed Shenandoah heap and call it architecture.
Advanced Usage and Diagnostics
ZGC: tune only after headroom is proven insufficient
- If latency spikes correlate with allocation pressure, increase -Xmx first.
- Use -XX:ZAllocationSpikeTolerance when bursts, not steady state, are the problem.
- Use -XX:ZCollectionInterval or -XX:+ZProactive only when you have a measured reason to shape idle-time behavior.
- Use -XX:ZFragmentationLimit carefully; more aggressive compaction can trade CPU for recovered memory.
Generational Shenandoah: focus on pacing and degradation signals
- If logs show allocation pressure, inspect pacing before blaming raw pause time.
- Use -XX:ShenandoahPacingMaxDelay carefully; it can hide collector stress by stalling allocating threads longer.
- If degenerated or full GCs appear, increase heap, lower allocation pressure, or start GC earlier with Shenandoah-specific heuristics where supported.
- On classic Shenandoah tuning, -XX:ShenandoahMinFreeThreshold, -XX:ShenandoahAllocSpikeFactor, and -XX:ShenandoahGarbageThreshold are the first knobs to study.
Fast triage checklist
- If ZGC pauses are fine but throughput regresses, check heap headroom and page policy before touching advanced flags.
- If Shenandoah shows pacing and degenerated GCs, fix allocation pressure or heap sizing first.
- If both collectors regress, inspect safepoints, reference processing, JNI pressure, and application-level queueing.
- If logs are messy, normalize startup lines and log configs before the next test run.
Frequently Asked Questions
What changed for ZGC in JDK 25? +
Do I still need -XX:+UnlockExperimentalVMOptions for Generational Shenandoah on JDK 25? +
Which is easier to tune first: ZGC or Generational Shenandoah? +
What are the first GC logs I should enable for low-latency Java services? +
Get Engineering Deep-Dives in Your Inbox
Weekly breakdowns of architecture, security, and developer tooling — no fluff.
Related Deep-Dives
Java Heap Sizing in Containers
How to size -Xms and -Xmx under cgroup limits without sabotaging latency.
Developer ReferenceJFR vs Unified Logging for Performance Debugging
A field guide to choosing low-overhead telemetry for JVM incidents and benchmark runs.
Developer ReferenceG1GC Tuning Cheat Sheet
A fast-reference companion when low pause goals do not justify a concurrent collector rollout.