6.6 KiB
Generational ZGC vs G1 on JDK 25 — runnable benchmarks and internals
Companion repository for Generational ZGC on JDK 25: Benchmarks vs G1 on ankurm.com.
The blog post covers the concepts most people need. This repository covers everything — every flag, every failure mode, every measurement pitfall, with runnable code, real captured output, and the reasoning behind each choice.
Everything here was compiled and executed on java 25.0.3+9-LTS-195. Raw, unedited output for every
run is in results/.
Quick start
git clone https://ankurm.com/git.app/asmhatre/zgc-jdk25-benchmarks.git
cd zgc-jdk25-benchmarks
# Windows
pwsh scripts/run-all.ps1 -JavaHome 'C:\Program Files\Java\jdk-25.0.3'
# Linux / macOS
./scripts/run-all.sh /path/to/jdk-25
Or run a single piece — nothing but a JDK 25 is required for anything outside jmh/:
javac -d out $(find latency tuning internals analysis -name '*.java')
java -XX:+UseZGC -Xms2g -Xmx2g -cp out com.ankurm.zgc.latency.LatencyHarness
java -XX:+UseG1GC -Xms2g -Xmx2g -cp out com.ankurm.zgc.latency.LatencyHarness
Headline result
60 seconds, 20,000 req/s open loop, 2 GB heap, ~585 MB live set, ~328 MB/s allocation rate.
Full output: results/03-latency-zgc.txt,
results/03-latency-g1.txt.
| Metric | Generational ZGC | G1 |
|---|---|---|
| p50 response time | 0.006 ms | 0.005 ms |
| p90 | 0.009 ms | 0.009 ms |
| p99 | 0.029 ms | 8.042 ms |
| p99.9 | 1.437 ms | 95.169 ms |
| p99.99 | 23.675 ms | 126.949 ms |
| max | 32.178 ms | 133.018 ms |
| total stop-the-world time | 1.503 ms | 1,139.624 ms |
| longest single pause | 0.054 ms | 104.449 ms |
| Full GCs / evacuation failures | 0 / 0 | 4 / 16 |
G1 wins the median. ZGC wins everything past p99. The two are indistinguishable up to p90 — which is the part most "ZGC is faster" posts leave out.
What is in here
Code
| Module | What it demonstrates | Deps |
|---|---|---|
env/Env.java |
Environment capture; proves generational mode from JMX bean names | none |
latency/ |
Open-loop latency harness with coordinated-omission accounting | none |
tuning/AllocationStallDemo.java |
Provokes and measures ZGC allocation stalls via JFR | none |
internals/PageSizeDemo.java |
ZGC page classes vs G1 humongous, read from the live VM | none |
analysis/GcLogParser.java |
Unified-log parser that keeps pauses and concurrent phases apart | none |
jmh/ |
Throughput and barrier microbenchmarks | JMH 1.37 |
JMH benchmarks
| Class | Isolates |
|---|---|
AllocationBench |
TLAB allocation path at 64 B / 1 KB / 32 KB |
LoadBarrierBench |
ZGC's load barrier — pointer chase vs primitive sum control |
StoreBarrierBench |
Write barriers — old→young vs null vs old→old vs primitive control |
PromotionBench |
Medium-lived objects; the case where "most objects die young" fails |
Documentation
| Chapter | Covers |
|---|---|
| 1. The generational model | JEP 439 → 474 → 490 timeline, what ZGenerational does now, why there is no -Xmn |
| 2. Barriers | Colored pointers, load barrier, store barrier, remembered sets, ZBufferStoreBarriers |
| 3. Allocation stalls | The ZGC failure mode pause charts cannot see, and the 10 ms JFR threshold that hides it |
| 4. Tuning reference | Every ZGC product + diagnostic flag, and the G1 flags that still matter |
| 5. Logging and JFR | Reading -Xlog:gc*, the JMX bean trap, the JFR events worth enabling |
| 6. Methodology | Coordinated omission, timer granularity, the JMH -jvmArgs fork trap |
| 7. Corner cases | Compressed oops, large-page fragmentation, virtual thread stacks, containers, System.gc() |
| 8. Choosing a collector | A decision procedure, sizing guidance, when the answer is neither |
Six findings you will not read elsewhere
- G1's coordinated-omission gap on this workload is 2910x. A closed-loop harness reports G1's p99.9 as 0.033 ms. It is really 95.169 ms. → docs/06-methodology.md §6.1
- ZGC disables compressed oops. Every reference is 8 bytes instead of 4, so a reference-dense heap can grow 30–40% on switching collector, before any latency benefit. → docs/07-corner-cases.md §7.1
jdk.ZAllocationStallhas a 10 ms default JFR threshold. In a run whose mean stall was 4.33 ms, a default profile discards most of them. → docs/03-allocation-stalls.md §3.3new byte[4*1024*1024]gets a ZGC Large page, not a Medium one. The 16-byte array header pushes it over the medium-object limit, and it consumes an 8 MB page. → docs/07-corner-cases.md §7.2SoftMaxHeapSizeandZAllocationSpikeTolerancedid nothing under sustained overload. Measured: 32,200 / 31,967 / 32,442 stalls across baseline and both mitigations. They buy headroom for spikes; they cannot conjure heap that does not exist. → results/04-stalls-zgc-softmax.txt- The throughput ranking inverts under sustained promotion. G1 is 6.6% faster with no survivors; ZGC is 10.4% faster with 262,144 of them. "G1 wins throughput" only holds on the workload that suits generational collection best. → results/07-jmh-zgc.txt
Reference machine
| CPU | AMD Ryzen 5 5600U, 6 cores / 12 threads |
| RAM | 15.3 GB |
| OS | Windows 11 Home |
| JDK | java 25.0.3+9-LTS-195 (Oracle) |
| Heap | -Xms2g -Xmx2g for all latency runs; -Xms512m -Xmx512m for stall runs |
A laptop, not a server. The absolute numbers are illustrative; the relationships are what generalise. Run it on your own hardware — that is what it is for.
Licence
MIT. See LICENSE.