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
This commit is contained in:
137
README.md
Normal file
137
README.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Generational ZGC vs G1 on JDK 25 — runnable benchmarks and internals
|
||||
|
||||
Companion repository for **[Generational ZGC on JDK 25: Benchmarks vs G1](https://ankurm.com/generational-zgc-jdk-25-vs-g1/)**
|
||||
on [ankurm.com](https://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/`](results).
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
```console
|
||||
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/`:
|
||||
|
||||
```console
|
||||
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-zgc.txt),
|
||||
[`results/03-latency-g1.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`](env/Env.java) | Environment capture; proves generational mode from JMX bean names | none |
|
||||
| [`latency/`](latency/src/com/ankurm/zgc/latency) | **Open-loop** latency harness with coordinated-omission accounting | none |
|
||||
| [`tuning/AllocationStallDemo.java`](tuning/src/com/ankurm/zgc/tuning/AllocationStallDemo.java) | Provokes and measures ZGC allocation stalls via JFR | none |
|
||||
| [`internals/PageSizeDemo.java`](internals/src/com/ankurm/zgc/internals/PageSizeDemo.java) | ZGC page classes vs G1 humongous, read from the live VM | none |
|
||||
| [`analysis/GcLogParser.java`](analysis/src/com/ankurm/zgc/analysis/GcLogParser.java) | Unified-log parser that keeps pauses and concurrent phases apart | none |
|
||||
| [`jmh/`](jmh/src/main/java/com/ankurm/zgc/jmh) | Throughput and barrier microbenchmarks | JMH 1.37 |
|
||||
|
||||
### JMH benchmarks
|
||||
|
||||
| Class | Isolates |
|
||||
|---|---|
|
||||
| [`AllocationBench`](jmh/src/main/java/com/ankurm/zgc/jmh/AllocationBench.java) | TLAB allocation path at 64 B / 1 KB / 32 KB |
|
||||
| [`LoadBarrierBench`](jmh/src/main/java/com/ankurm/zgc/jmh/LoadBarrierBench.java) | **ZGC's load barrier** — pointer chase vs primitive sum control |
|
||||
| [`StoreBarrierBench`](jmh/src/main/java/com/ankurm/zgc/jmh/StoreBarrierBench.java) | **Write barriers** — old→young vs null vs old→old vs primitive control |
|
||||
| [`PromotionBench`](jmh/src/main/java/com/ankurm/zgc/jmh/PromotionBench.java) | Medium-lived objects; the case where "most objects die young" fails |
|
||||
|
||||
### Documentation
|
||||
|
||||
| Chapter | Covers |
|
||||
|---|---|
|
||||
| [1. The generational model](docs/01-generational-model.md) | JEP 439 → 474 → 490 timeline, what `ZGenerational` does now, why there is no `-Xmn` |
|
||||
| [2. Barriers](docs/02-barriers.md) | Colored pointers, load barrier, store barrier, remembered sets, `ZBufferStoreBarriers` |
|
||||
| [3. Allocation stalls](docs/03-allocation-stalls.md) | **The ZGC failure mode pause charts cannot see**, and the 10 ms JFR threshold that hides it |
|
||||
| [4. Tuning reference](docs/04-tuning-reference.md) | Every ZGC product + diagnostic flag, and the G1 flags that still matter |
|
||||
| [5. Logging and JFR](docs/05-logging-and-jfr.md) | Reading `-Xlog:gc*`, the JMX bean trap, the JFR events worth enabling |
|
||||
| [6. Methodology](docs/06-methodology.md) | Coordinated omission, timer granularity, the JMH `-jvmArgs` fork trap |
|
||||
| [7. Corner cases](docs/07-corner-cases.md) | Compressed oops, large-page fragmentation, virtual thread stacks, containers, `System.gc()` |
|
||||
| [8. Choosing a collector](docs/08-choosing.md) | A decision procedure, sizing guidance, when the answer is neither |
|
||||
|
||||
---
|
||||
|
||||
## Five findings you will not read elsewhere
|
||||
|
||||
1. **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](docs/06-methodology.md)
|
||||
2. **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](docs/07-corner-cases.md)
|
||||
3. **`jdk.ZAllocationStall` has a 10 ms default JFR threshold.** In a run whose mean stall was
|
||||
3.98 ms, a default profile discards most of them.
|
||||
→ [docs/03-allocation-stalls.md §3.3](docs/03-allocation-stalls.md)
|
||||
4. **`new 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.2](docs/07-corner-cases.md)
|
||||
5. **`SoftMaxHeapSize` and `ZAllocationSpikeTolerance` did 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](results/04-stalls-zgc-softmax.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](LICENSE).
|
||||
Reference in New Issue
Block a user