Home Posts JVM 25 GC Tuning Cheat Sheet: ZGC vs Shenandoah [2026]
Developer Reference

JVM 25 GC Tuning Cheat Sheet: ZGC vs Shenandoah [2026]

JVM 25 GC Tuning Cheat Sheet: ZGC vs Shenandoah [2026]
Dillip Chowdary
Dillip Chowdary
Tech Entrepreneur & Innovator · May 22, 2026 · 9 min read

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.
DimensionZGCGenerational ShenandoahEdge
JDK 25 statusGenerational-only by defaultGenerational mode is a product featureTie
Simplest enable flag-XX:+UseZGC-XX:+UseShenandoahGC -XX:ShenandoahGCMode=generationalZGC
Main first tuning lever-Xmx headroom-Xms/-Xmx plus pacing and page policyZGC
Low-pause design goalPause work kept under about 1 msMost workloads commonly land in very low single-digit ms to ~10 ms territoryZGC
Large-page upsideRecommended on large heapsStrongly recommended on large heapsTie
Failure-mode visibilityClean logging, fewer manual heuristicsPacing and degenerated/full-GC ladder is very explicitShenandoah
Operational frictionLowerHigher, and depends on a Shenandoah-enabled buildZGC

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.
Watch out: Do not cargo-cult old launch flags. A legacy -XX:+ZGenerational or -XX:-ZGenerational line is a migration smell on JDK 25, and generational Shenandoah examples copied from JDK 24 should drop -XX:+UnlockExperimentalVMOptions.

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.

ShortcutActionUse case
/Focus the live filterJump straight to a flag or config pattern
EscClear the filterReset the sheet quickly during incident review
g cGo to Command Cheat SheetFast section navigation
g zGo to ZGC commandsCollector-specific jump
g sGo to Shenandoah commandsCollector-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.
Pro tip: If a low-latency collector looks bad in production-like tests, compare allocation rate, live-set size, root-set size, and page policy before you compare exotic flags. Those four usually explain more than folklore about one collector being “faster.”

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? +
By JDK 25, ZGC should be treated as generational-only. The practical effect is simple: use -XX:+UseZGC as your normal enable flag and remove legacy ZGenerational toggles from old launch scripts.
Do I still need -XX:+UnlockExperimentalVMOptions for Generational Shenandoah on JDK 25? +
No. Generational Shenandoah became a product feature in JDK 25, so the old experimental unlock flag is no longer required for that mode. Your clean enable line is -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational on a Shenandoah-enabled build.
Which is easier to tune first: ZGC or Generational Shenandoah? +
Usually ZGC. Oracle's tuning guidance emphasizes that -Xmx headroom is the main knob, while Shenandoah exposes more explicit heuristics and pacing behavior that can be useful but also increases operator surface area.
What are the first GC logs I should enable for low-latency Java services? +
Start with -Xlog:gc* for both collectors. For Shenandoah-heavy investigations, add -Xlog:gc+ergo,gc+stats,safepoint so you can see trigger decisions, pacing, degenerated GC, full GC, and non-GC stop-the-world activity.

Get Engineering Deep-Dives in Your Inbox

Weekly breakdowns of architecture, security, and developer tooling — no fluff.

Found this useful? Share it.