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

139 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |
| [`jdk27-memory/`](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](https://ankurm.com/jdk-27-compact-object-headers-g1-default-memory-spring-boot/) | Spring Boot 4.1.1, three JDKs |
### 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).