1
0
Files
zgc-jdk25-benchmarks/README.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

6.3 KiB
Raw Permalink Blame History

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

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
  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
  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
  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
  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

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.