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:
115
docs/01-generational-model.md
Normal file
115
docs/01-generational-model.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# 1. The generational model, and what JDK 25 actually changed
|
||||
|
||||
> Companion notes for [Generational ZGC on JDK 25: Benchmarks vs G1](https://ankurm.com/generational-zgc-jdk-25-vs-g1/).
|
||||
> Everything here is verified against `java 25.0.3+9-LTS-195`.
|
||||
|
||||
## 1.1 The one-paragraph history
|
||||
|
||||
| Release | What happened |
|
||||
|---|---|
|
||||
| JDK 11 | ZGC arrives as an experimental, **non-generational** collector (JEP 333). One generation, whole-heap marking every cycle. |
|
||||
| JDK 15 | ZGC becomes a production feature (JEP 377). |
|
||||
| JDK 21 | **Generational ZGC** ships as an opt-in mode behind `-XX:+ZGenerational` (JEP 439). |
|
||||
| JDK 23 | Generational becomes the **default** (JEP 474). `-XX:-ZGenerational` still gets you the old one. |
|
||||
| JDK 24 | The non-generational mode is **removed** (JEP 490). `ZGenerational` becomes an obsolete flag. |
|
||||
| JDK 25 | First **LTS** where ZGC is generational-only. This is the release most teams will actually adopt. |
|
||||
|
||||
Verify the removal yourself — the JVM says so out loud:
|
||||
|
||||
```console
|
||||
$ java -XX:+UseZGC -XX:+ZGenerational -version
|
||||
Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option ZGenerational; support was removed in 24.0
|
||||
java version "25.0.3" 2026-04-21 LTS
|
||||
```
|
||||
|
||||
That warning matters operationally: if your start-up scripts still carry `-XX:+ZGenerational` from a
|
||||
JDK 21 experiment, JDK 25 will *not* fail fast. It warns and continues. Grep your deployment
|
||||
manifests.
|
||||
|
||||
## 1.2 What "generational" buys, concretely
|
||||
|
||||
The weak generational hypothesis: most objects die young. A collector that exploits it can reclaim
|
||||
the young generation without looking at the old one.
|
||||
|
||||
Non-generational ZGC could not do that. Every cycle marked the **entire** heap. With a 600 MB live
|
||||
set and a 300 MB/s allocation rate, that means repeatedly tracing 600 MB of objects that you
|
||||
already know are alive, in order to reclaim garbage that lives in a few megabytes of recently
|
||||
allocated pages.
|
||||
|
||||
Generational ZGC splits this into:
|
||||
|
||||
- a **minor (young) collection** that marks and relocates only young pages, and
|
||||
- a **major (old) collection** that additionally handles the old generation, run far less often.
|
||||
|
||||
You can see the split in the JMX bean names, which is the cheapest possible proof of what you are
|
||||
running:
|
||||
|
||||
```console
|
||||
collector : ZGC Minor Cycles + ZGC Minor Pauses + ZGC Major Cycles + ZGC Major Pauses
|
||||
```
|
||||
|
||||
Four beans, not two. Under non-generational ZGC there was one cycle bean and one pause bean.
|
||||
`env/Env.java` in this repository prints exactly this and derives `generational? yes`.
|
||||
|
||||
## 1.3 The cost of the split: you now need a write barrier
|
||||
|
||||
There is no free lunch. To collect young without scanning old, the collector must know about every
|
||||
**old-to-young reference**. Those are created by ordinary application code:
|
||||
|
||||
```java
|
||||
oldCache[i] = new Payload(); // an old object now points at a brand-new young object
|
||||
```
|
||||
|
||||
If a young collection ignored `oldCache`, it would conclude `new Payload()` is unreachable and free
|
||||
a live object.
|
||||
|
||||
So generational ZGC added a **store barrier** — code the JIT injects around reference stores, which
|
||||
records the cross-generational edge. Non-generational ZGC had *no* store barrier at all; it only
|
||||
had a load barrier. This is the single biggest structural change in JEP 439, and it is why
|
||||
generational ZGC has slightly different throughput characteristics from the old one on
|
||||
store-heavy code. See [02-barriers.md](02-barriers.md).
|
||||
|
||||
You can see the mechanism named in the VM's own diagnostic flags:
|
||||
|
||||
```console
|
||||
$ java -XX:+UseZGC -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version | findstr Z
|
||||
bool ZBufferStoreBarriers = true {diagnostic} {default}
|
||||
int ZTenuringThreshold = -1 {diagnostic} {default}
|
||||
bool ZUseMediumPageSizeRange = true {diagnostic} {default}
|
||||
```
|
||||
|
||||
- `ZBufferStoreBarriers` — the store barrier does not update a remembered set inline; it appends to
|
||||
a per-thread buffer that is drained later. That is what keeps the barrier cheap.
|
||||
- `ZTenuringThreshold = -1` — **adaptive**. ZGC ages objects like any generational collector, but
|
||||
unlike G1's `MaxTenuringThreshold` (default 15, a documented product flag), ZGC's is a diagnostic
|
||||
flag that defaults to "let the collector decide". Pinning it is almost always a mistake; knowing
|
||||
it exists is occasionally the answer to "why did my promotion rate change after an upgrade".
|
||||
|
||||
## 1.4 Where the generations physically live
|
||||
|
||||
ZGC does not have a contiguous eden / survivor / old layout the way Parallel or (loosely) G1 does.
|
||||
Generation membership is a property of a **page**, and pages are 2 MB / medium / large units
|
||||
scattered across the reserved address space. A young page whose objects survive enough cycles is
|
||||
*promoted* by relocating its live objects into an old page.
|
||||
|
||||
This has a practical consequence people trip over: **there is no `-Xmn` for ZGC**. You cannot size
|
||||
the young generation. ZGC decides how much of the heap is young dynamically, based on allocation
|
||||
rate and how much headroom `SoftMaxHeapSize` leaves it. If you are used to tuning G1 with
|
||||
`-XX:NewRatio` or `-XX:G1NewSizePercent`, that entire toolbox is gone. The ZGC equivalent of "give
|
||||
the young generation more room" is "give the heap more room, or lower `SoftMaxHeapSize` so
|
||||
collection starts earlier".
|
||||
|
||||
## 1.5 What this means for a migration
|
||||
|
||||
| If you relied on… | On generational ZGC you get… |
|
||||
|---|---|
|
||||
| `-Xmn`, `-XX:NewRatio`, `-XX:SurvivorRatio` | Nothing. Ignored or unavailable. Size the whole heap instead. |
|
||||
| `-XX:MaxTenuringThreshold` | `ZTenuringThreshold`, diagnostic, adaptive by default. |
|
||||
| `-XX:+ZGenerational` | An obsolete-flag warning. Remove it. |
|
||||
| GC pause charts as your latency SLI | A misleading green dashboard. See [03-allocation-stalls.md](03-allocation-stalls.md). |
|
||||
| `-XX:+UseStringDeduplication` | Still supported on ZGC (since JDK 18), not G1-only any more. |
|
||||
|
||||
---
|
||||
|
||||
Next: [02-barriers.md](02-barriers.md) — colored pointers, load barriers, store barriers and
|
||||
remembered sets, with the microbenchmark that prices each one.
|
||||
Reference in New Issue
Block a user