Generational ZGC shipped as an option in JDK 21 and became the default in JDK 23. What does splitting a concurrent, colored-pointer collector into young and old generations actually change in its implementation, and which production problem motivated the change?
answer
- Old design: full-heap marking every cycle → CPU + headroom heavy
- New: store barrier added (load barrier was not enough)
- Remembered sets = per-page bitmaps of old→young slots, double-buffered
- Wider pointer color; multi-mapping dropped
- Young + old both fully concurrent; aging/promotion; fewer allocation stalls
basics
~20 sIt lets ZGC collect a small young space frequently instead of marking the whole heap each cycle. That required a new store barrier and remembered sets to track old-to-young references. The payoff: much less CPU and heap headroom for a given allocation rate, and fewer allocation stalls.
solid answer
~50 sNon-generational ZGC had to mark and relocate across the **entire heap** on every cycle. Under a high allocation rate that meant frequent full-heap cycles, which burned GC CPU and demanded a lot of free headroom so the collector could finish before the application ran out of memory. When it lost that race, threads hit allocation stalls. The generational design collects a young space on its own, so most reclamation touches only recently allocated memory. Implementation changes: - **A store barrier was added.** Non-generational ZGC needed only load barriers; tracking old-to-young references requires intercepting reference *stores*. - **Remembered sets** (per-page bitmaps of slots in old holding young references) let a young collection find its roots without scanning the old generation. - **Colored pointers carry more state** — per-generation mark/remap and remembered-set bits — and the layout changed; heap multi-mapping was dropped. - Young and old collections are separate, both fully concurrent, with **aging and promotion** between them. Same sub-millisecond pauses, materially better efficiency.
code
text · 7 lines# JDK 21 / 22 — generational mode is opt-in
java -XX:+UseZGC -XX:+ZGenerational -Xmx8g -Xlog:gc -jar app.jar
# JDK 23 — generational is the default; the flag is redundant
java -XX:+UseZGC -Xmx8g -Xlog:gc -jar app.jar
# JDK 24+ — non-generational mode no longer exists; -XX:-ZGenerational is not acceptedgo deeper
Know that modern ZGC has young and old generations, that this is the default on current JDKs, and that it makes the collector cheaper rather than the pauses shorter.
Name the concrete additions — store barrier, remembered sets, promotion — and the motivation: avoid marking the whole heap every cycle.
Connect it to operations: lower GC CPU and heap headroom for a given allocation rate, and materially fewer allocation stalls under load spikes.
Treat it as a capacity lever: the same latency target now costs less memory and CPU, which changes instance sizing and the safety margin you budget for traffic bursts.
## The problem with a single-generation concurrent collector Non-generational ZGC treated the heap as one space. Every cycle it marked the whole reachable graph and evacuated a relocation set drawn from anywhere. That is correct and gives heap-independent pauses, but it is *inefficient* for the allocation pattern of typical server applications, where the great majority of objects become garbage almost immediately. Two costs followed. First, **CPU**: marking the entire live set to reclaim mostly short-lived garbage means repeatedly re-traversing long-lived data that has not changed. Second, **headroom**: a full-heap cycle takes a while, and during that time the application keeps allocating, so the heap must hold enough free space to absorb an entire cycle's worth of allocation. Push allocation rate up or headroom down and the collector loses the race — at which point allocating threads **stall**, waiting for memory. Stalls are ZGC's failure mode, and they are far longer than its pauses. Operators compensated by over-provisioning heap and CPU. Generational ZGC (JEP 439) removes most of that tax. ## What generations change *in the implementation* Splitting into young and old is not a configuration flag over the same machinery; it required real additions: **1. A store barrier.** The original ZGC had one barrier, on reference loads. A generational collector must know when the old generation is made to point at a young object, or a young collection would miss roots and free live data. That knowledge can only be gathered at reference *stores*, so a store barrier was introduced. ZGC now runs both. **2. Remembered sets.** The barrier's output is recorded in per-page bitmaps marking which slots in old-generation pages hold references into young. A young collection scans thread roots plus those recorded slots — cheap and proportional to the mutation rate, not to the size of the old generation. The implementation uses double-buffered bitmaps so that a collection can consume a stable snapshot while the application keeps recording new entries. **3. A richer pointer color.** Colored pointers now encode per-generation state (marked/remapped for young and for old separately) plus remembered-set information, so the bit layout was reworked. As part of that redesign, the heap **multi-mapping** trick used by non-generational ZGC on Linux was dropped; barriers now hand clean addresses to the compiled code. A visible side effect is that ZGC processes no longer report the inflated virtual-memory sizes that used to confuse monitoring. **4. Two independent, fully concurrent collections.** Young collections run often and are cheap; old collections run rarely and reclaim what survives. Both keep the same pause structure and the same sub-millisecond targets — importantly, generational ZGC did *not* introduce a stop-the-world young pause the way older generational collectors have. **5. Aging and promotion.** Objects that survive some number of young collections are promoted to old, so long-lived data stops being re-marked and re-copied on every young cycle. Promotion policy is adaptive rather than something you tune with survivor-ratio flags. ## Why it pays off A young collection touches a small live set: the survivors of recent allocation. Reclaiming most garbage therefore costs a fraction of a full-heap cycle. Concretely, for a given allocation rate a generational ZGC deployment needs **less GC CPU** and **much less heap headroom**, and tolerates **higher allocation rates** before stalling. Equivalently, on the same hardware you can run a smaller heap or absorb bigger traffic spikes. The cost is a second barrier on reference stores and extra metadata for remembered sets — a good trade in almost every measured workload, which is why the JDK made it the default and then removed the non-generational mode entirely. ## Operating it On JDK 23 and later, `-XX:+UseZGC` gives you the generational collector; there is nothing else to enable. On JDK 21 and 22 it was opt-in with an additional flag, and the non-generational mode was deprecated in 22 and removed in JDK 24 (JEP 490). Tuning remains deliberately minimal: set a heap size with headroom, give the collector CPU, and watch the GC log for allocation stalls, which are the signal that the collector is losing the race. ## The distinction to keep straight in an interview The interesting answer is not "most objects die young, so generations help" — that is true of every generational collector. The interesting answer is *what a concurrent, relocating, colored-pointer collector had to grow in order to become generational*: a store barrier it previously did not need, remembered sets, a wider pointer color, a promotion policy, and two independent concurrent cycles — all without giving up sub-millisecond pauses.
- Why did adding generations require a store barrier when the original ZGC needed only a load barrier?A young-only collection must treat references from old objects into young objects as roots; otherwise it would consider live young objects unreachable. The only moment the JVM can observe such a reference being created is when it is written, so a barrier on reference stores is required to record the slot in a remembered set. Load barriers cannot supply that information because they fire on reads, which say nothing about which old slots currently point into young.
- Did making ZGC generational increase its pause times?No. Both young and old collections keep the same phase structure with the same short root-oriented pauses, so the sub-millisecond target is unchanged. What changed is efficiency: less GC CPU and less heap headroom for the same allocation rate, in exchange for the extra store barrier and remembered-set metadata.
saying these in an interview costs you the question
- Answering only with the observation that most objects die young, without describing what the collector had to add.
- Believing generational ZGC introduced a stop-the-world young collection like older generational collectors.
- Thinking remembered sets are maintained by the load barrier rather than by the newly added store barrier.
- Assuming you still need -XX:+ZGenerational on modern JDKs, or that the non-generational mode is still available.
- Claiming generations made pauses shorter; they made the collector cheaper, not the pauses smaller.