1
0

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:
2026-07-31 08:47:16 +05:30
commit e189da79ba
45 changed files with 12572 additions and 0 deletions

137
README.md Normal file
View 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 3040% 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).