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

102 lines
6.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-zgc.txt),
[`results/02-flags-g1.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:
```console
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:
```console
$ 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](03-allocation-stalls.md) §3.3).
---
Next: [05-logging-and-jfr.md](05-logging-and-jfr.md).