# 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 (3–5) 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, 1–32 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).