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