1
0
Files
zgc-jdk25-benchmarks/docs/04-tuning-reference.md
Ankur e189da79ba Generational ZGC vs G1 on JDK 25: benchmarks, internals and captured results
Complete companion suite for the ankurm.com guide. Everything was compiled and
executed on java 25.0.3+9-LTS-195 (AMD Ryzen 5 5600U, Windows 11); every file
under results/ is unedited program output.

Code
  latency/    open-loop latency harness that measures from intended arrival, so
              coordinated omission is accounted for rather than hidden. Reports
              response time and service time side by side; the gap reached 2910x
              on G1.
  jmh/        four microbenchmarks isolating one mechanism each: TLAB allocation,
              the ZGC load barrier (with a primitive-load control), the write
              barrier (with null / old-to-old / primitive controls), and promotion
              pressure.
  tuning/     provokes ZGC allocation stalls and reads its own JFR recording back,
              grouped by page class. jdk.ZAllocationStall is enabled without a
              threshold, because the 10 ms default hides most stalls.
  internals/  ZGC page classes vs G1 humongous, read from the live VM via
              HotSpotDiagnosticMXBean and jdk.ZPageAllocation events.
  analysis/   unified GC log parser that keeps stop-the-world pauses and
              concurrent phases in separate buckets.
  env/        environment capture; proves generational mode from JMX bean names.

Docs
  Eight chapters covering the JEP 439/474/490 timeline, colored pointers and both
  barriers, allocation stalls, a full flag reference, logging and JFR, benchmark
  methodology, corner cases, and a decision procedure.

Headline result (60 s, 20k req/s, 2 GB heap, ~585 MB live)
  p50               ZGC 0.006 ms   G1 0.005 ms
  p99.9             ZGC 1.437 ms   G1 95.169 ms
  total STW time    ZGC 1.503 ms   G1 1,139.624 ms
2026-07-31 08:47:16 +05:30

6.3 KiB
Raw Permalink Blame History

4. Tuning reference (JDK 25)

Everything below was read out of a live java 25.0.3+9-LTS-195 with -XX:+PrintFlagsFinal and HotSpotDiagnosticMXBean. Raw dumps: results/02-flags-zgc.txt, results/02-flags-g1.txt.

The general rule for ZGC is unusual and worth stating plainly: there is almost nothing to tune. The design goal was to remove knobs, not add them. If you find yourself reaching for a fourth flag, the answer is usually heap size or application allocation behaviour.

4.1 ZGC — product flags (supported, safe to use)

Flag Default (2 GB heap) What it does When to touch it
-XX:+UseZGC off Selects ZGC. Always generational on JDK 24+.
-XX:SoftMaxHeapSize = -Xmx Soft target the GC heuristic aims for; may be exceeded up to -Xmx rather than stall. manageable — changeable at runtime via jcmd VM.set_flag. Set below -Xmx to create spike headroom. The single highest-value ZGC flag.
-XX:ZAllocationSpikeTolerance 2.0 Multiplier on observed allocation rate when predicting when to start a cycle. Raise (35) for bursty services that stall on ramp-up.
-XX:ZCollectionIntervalMinor -1.0 (off) Force a young cycle every N seconds. Very spiky duty cycles where rate prediction keeps failing.
-XX:ZCollectionIntervalMajor -1.0 (off) Force an old cycle every N seconds. Slow, steady old-gen growth you want amortised.
-XX:ZCollectionInterval 0.0 (off) Legacy combined interval. Prefer the Minor/Major pair.
-XX:ZCollectionIntervalOnly false Collect only on the interval, ignoring the allocation-rate heuristic. Benchmarks and reproducible experiments. Not production.
-XX:ZFragmentationLimit 5.0 (%) A page is added to the relocation set when this much of it is garbage. Lower = more aggressive compaction, more CPU. Rarely worth changing.
-XX:ZYoungCompactionLimit 25.0 (%) Equivalent threshold for young pages. Rarely.
-XX:+ZUncommit true, but false when -Xms == -Xmx Return unused heap to the OS. Leave on. Note the automatic disable — see §4.4.
-XX:ZUncommitDelay 300 (s) How long a page must stay unused before being uncommitted. Lower it in bursty container workloads to give memory back sooner.

4.2 ZGC — diagnostic flags (need -XX:+UnlockDiagnosticVMOptions)

These appear in no tuning guide. You should almost never set them. You should know they exist, because they name the mechanisms.

Flag Default Meaning
ZBufferStoreBarriers true The generational store barrier appends to a per-thread buffer that is drained later, rather than updating remembered sets inline.
ZTenuringThreshold -1 Age at which young objects are promoted. -1 = adaptive. ZGC's answer to MaxTenuringThreshold.
ZUseMediumPageSizeRange true JDK 25: the medium page size is a range chosen at runtime, not a constant. This is why there is no ZPageSizeMedium product flag to read.
ZStressFastMediumPageAllocation false Test hook.
ZIndexDistributorStrategy 0 Internal work-distribution strategy for parallel phases.
ZStatisticsInterval 10 (s) Interval for -Xlog:gc+stats.

4.3 G1 — the flags that still matter on JDK 25

Flag Default (2 GB heap) Notes
-XX:+UseG1GC default collector on server-class machines
-XX:MaxGCPauseMillis 200 A goal, not a guarantee. G1 sizes the young generation to try to hit it. Setting it very low (e.g. 5) makes G1 shrink young until it collects constantly — a classic self-inflicted throughput cliff.
-XX:G1HeapRegionSize ergonomic, 1 MB at 2 GB heap Power of two, 132 MB, roughly heap/2048. Determines the humongous threshold (half a region).
-XX:G1NewSizePercent / G1MaxNewSizePercent 5 / 60 Bounds on young generation size.
-XX:InitiatingHeapOccupancyPercent 45 When to start a concurrent cycle. G1 adapts this by default (-XX:-G1UseAdaptiveIHOP to pin it).
-XX:G1MixedGCLiveThresholdPercent 85 Old regions above this liveness are not collected in mixed GCs.
-XX:MaxTenuringThreshold 15 Real, documented, product flag — unlike ZGC's.

The humongous threshold is the G1 flag most people should check and don't. At a 2 GB heap the region size is 1 MB, so anything over 512 KB is humongous: allocated straight into the old generation, spanning whole regions, never given the cheap young-collection path. A service that allocates 1 MB buffers is generating humongous objects on every request. Confirmed on this machine:

G1HeapRegionSize     : 1,048,576 bytes (1 MB)
=> G1 humongous threshold: objects larger than 524,288 bytes (512 KB) go straight to old gen

Raising -XX:G1HeapRegionSize=8m moves the threshold to 4 MB and can transform such a workload.

4.4 Two ergonomic surprises worth knowing

ZUncommit silently turns itself off when -Xms == -Xmx. Verified:

$ java -XX:+UseZGC -Xms2g -Xmx2g ... PageSizeDemo
  ZUncommit                    = false

but with a range (-Xms512m -Xmx2g) it is true. This is logical — you asked for a fixed heap, so there is nothing to give back — but it surprises people who set a fixed heap and expect memory to be returned to a container.

SoftMaxHeapSize defaults to -Xmx, which means "no reserve". Its default is ergonomic and equal to max heap, so out of the box it does nothing. It only becomes useful when you set it.

4.5 A starting configuration

For a latency-sensitive JVM service on JDK 25, this is a defensible starting point — not because these values are optimal, but because each one is there for a stated reason:

-XX:+UseZGC
-Xms8g -Xmx8g                          # fixed heap: no resize noise
-XX:SoftMaxHeapSize=6g                 # 25% reserve for allocation spikes
-XX:ZAllocationSpikeTolerance=3        # this service ramps fast
-Xlog:gc*,gc+alloc=debug:file=gc.log:time,uptime,level,tags:filecount=5,filesize=50m
-XX:StartFlightRecording=settings=profile,filename=app.jfr,maxsize=500m

plus a .jfc override setting jdk.ZAllocationStall threshold to 0 ms (see 03-allocation-stalls.md §3.3).


Next: 05-logging-and-jfr.md.