Files
zgc-jdk25-benchmarks/README.md
T
Claude e83c9949d8 Add jdk27-memory: compact object headers and G1-everywhere on a Spring Boot service
Spring Boot 4.1.1 catalog service measured on JDK 25, 26 and 27: live heap 320 MB to 273 MB with compact headers, per-class bytes per instance, container-sizing sweeps with real cgroup limits, 1-CPU Serial vs G1 with Native Memory Tracking, removed flags (MaxRAM, UseCompressedClassPointers), and a kernel OOM-kill check. Outputs in jdk27-memory/output.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a
2026-09-24 15:09:55 +00:00

6.7 KiB
Raw 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
jdk27-memory/ Spring Boot 4.1 service measured on JDK 25, 26 and 27: compact object headers (JEP 534) and G1 everywhere (JEP 523), heap footprint, container sizing with real cgroup limits. Its README and output/ replace a docs/ folder; the write-up is the post Spring Boot 4.1.1, three JDKs

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 30–40% 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.