gc-fundamentals · git:20260903.f199530 · 2026-09-03 · sha256 da9e4245072279ab
gc-fundamentals git:20260903.f199530A
Immutable. This exact content never changes and is served at /api/v1/blob/da9e4245072279ab.
--- name: gc-fundamentals description: > How JVM garbage collectors work, as the mental model behind every GC diagnosis: the generational hypothesis and where it fails, mark-sweep versus mark-compact versus copying, why collection cost tracks survivors rather than allocation, write barriers, safepoints and Time-To-SafePoint, and the JDK 25 collector landscape. Use when explaining why a collection is expensive, when the reported pause does not match the latency the client feels, when a young pause is long although little survived, when a cache or pool breaks the generational assumption, when humongous allocations appear, or when comparing collectors written before JDK 23. Does not cover choosing a collector or sizing the heap (jvm-gc-tuning), parsing the log (gc-log-analysis), or the non-heap regions (jvm-memory-regions). Per-collector internals are g1-internals and zgc-and-shenandoah, and the safepoint mechanism is safepoints. --- # GC Fundamentals ## Purpose Supply the mechanism behind GC symptoms, so a diagnosis explains rather than describes. The failure this prevents is tuning a collector to fix something the collector is not doing — most reported "GC problems" are the collector behaving correctly given how much live data it is handed. ## Workflow 1. **Separate pause duration from pause frequency.** Duration points at the live set, at one phase of the pause, or at the collector. Frequency points _upstream_, at allocation rate. 2. **Ask what survived, then inspect the non-copy work.** The evacuation component tracks surviving objects (and their shape) much more closely than allocated bytes. A large mostly-dead Eden can therefore be cheap to reclaim, but root scanning, remembered-set work, reference processing, worker imbalance and page effects can still dominate; do not infer total pause time from mortality alone. 3. **Read the phases before the total.** A young pause is root scanning, remembered-set scanning, copying and reference processing, and each grows for a different reason. `-Xlog:gc+phases=debug` names the one that grew; the mapping from phase to cause is in `references/collector-mechanisms.md`. 4. **Check whether the generational hypothesis holds here.** It fails in caches, object pools, and any system where high downstream latency keeps many requests in flight. Recognising the failure is part of the diagnosis. 5. **Reconcile the reported pause with the observed one.** The GC log's pause does **not** include Time-To-SafePoint. If the log says 16 ms and the client felt 200 ms, the GC event alone does not explain the observation — correlate `-Xlog:safepoint`, request traces and host scheduling. On 25, bulk operations with sparse polls (a large `arraycopy` or array zeroing), VM transitions and CPU starvation remain relevant; counted loops are usually mitigated by loop strip mining, not categorically impossible. 6. **Look upstream before touching a flag.** By `N = λ × R`, slower downstream calls mean more requests in flight, which means more live young objects. Expensive GC is frequently a symptom of slowness elsewhere. 7. **Match the symptom to a mechanism** with the table in `references/diagnosis-and-versions.md`, then hand off to the skill that owns the fix. ## Rules - The generational hypothesis is an empirical observation, not a theorem. Most objects die young — except where your design says otherwise. - Evacuation cost often follows survivors, counted in objects. In one controlled Temurin 25.0.3 experiment, a young pause evacuating ten million live 24-byte nodes spent 328 ms in `Evacuate Collection Set`; the same list with `-XX:+UseCompactObjectHeaders` (JEP 519, product on 25, off by default) was 33% smaller in bytes (230 MB → 154 MB) and its full collection took approximately the same 240 ms. This illustrates one object-count-dominated workload; it does not prove that bytes never matter. Smaller objects can reduce collection frequency and copy/cache costs, so repeat the experiment with the production object graph and collector phases. - The three base algorithms are mark-sweep (does not move, fragments), mark-compact (compacts, expensive because it moves) and copying (no fragmentation, reserves space). Every real collector combines them per region and generation. - Writing a reference is never just writing a reference: the write barrier maintains the card table or remembered set. Through 25, G1 uses an SATB pre-barrier plus a filtered, queued post-barrier; its cost relative to a simple card mark depends on the workload and must be measured. JEP 522 (JDK 26) replaces the queue with a second card table. The concurrent collectors are not "load barriers instead": generational ZGC and generational Shenandoah carry store barriers as well. - Promotion is a one-way door. An object copied to old stays until a mixed or full collection reaches it, and while dead there it keeps every young object it references alive through the card table — nepotism. Premature promotion therefore raises the cost of every _young_ collection that follows, not only the old one. - After a concurrent marking cycle the heap is not the live set. SATB marks everything allocated during the cycle live, so the post-cycle occupancy overstates retention by the allocation of one cycle; judge retention from a full collection or the trend across cycles. - Short pause is a requirement, not a universal virtue. ZGC and Shenandoah move much GC work out of the stop-the-world path in exchange for barriers, concurrent CPU and heap headroom for the cycle to outrun allocation. For batch work with no latency SLO, Parallel is a candidate because its stop-the-world design can maximize throughput; only a representative run establishes which collector delivers more useful work per hour. - `MaxGCPauseMillis` is a target, not a guarantee — and **lowering it can make everything worse**: it shrinks the young generation, collections become more frequent, objects get less time to die in Eden, and premature promotion rises. - A generational young collection need not scan the whole heap, but its pause is not independent of heap size: ergonomics may enlarge the young generation, root and remembered-set structures can grow, and more survivors or old-to-young edges add work. Full-heap stop-the-world work has a stronger live-set/heap dependency. ZGC and Shenandoah reduce that dependency for their normal concurrent cycles, but degeneration, allocation failure and Full GC remain workload- and implementation-dependent fallbacks. - Unplanned Full GC should be absent from the steady-state latency budget of an online service; an explicit maintenance or batch collection may be acceptable if it is outside the SLO and measured. On 25 G1 logs an evacuation failure as `(Evacuation Failure: Allocation)` on the pause line: no free region was left to copy into. Old having filled up is the usual reason, humongous fragmentation and the `G1ReservePercent` (10) reserve being consumed are the others; raising the heap is palliative, the question is why the free list emptied. - On the JDK 25 baseline, **ZGC is generational by definition** — `-XX:+ZGenerational` no longer exists (JEP 490, JDK 24) — and **generational Shenandoah is product** (JEP 521). Any collector comparison written before JDK 23 needs redoing. - **Do not plan an upgrade around an undelivered JEP.** As of 2026-09-03, JEP 523 (make G1 the default in every environment) is only Candidate and has no target release. On the verified JDK 25 baseline, a JVM that sees one CPU picks Serial (`-XX:ActiveProcessorCount=1` logs `Using Serial`). Verify ergonomics with `-Xlog:gc+init` on the exact vendor build rather than assuming JDK 27 changes it. ## References - [Collector mechanisms](references/collector-mechanisms.md) — the algorithms, the tri-colour invariants, generations and promotion, the anatomy of a young pause with the 25.0.3 phase names, barriers per collector, allocation, humongous objects, reference processing, what the concurrent collectors trade, and the landscape table. Read when the question is _why_ a collection costs what it costs. - [Safepoints and Time-To-SafePoint](references/safepoints.md) — what a safepoint is, why TTSP is excluded from the reported pause, the 25 log line, and the causes of high TTSP with the measurements that rank them. Read when the pause the client felt does not match the pause in the log. - [Diagnosis and versions](references/diagnosis-and-versions.md) — the symptom → mechanism → how to distinguish → what to measure → owner table, the delivered JDK 9–26 collector timeline plus explicitly labelled proposals with JEP numbers, and the flag lifecycle. Read when a symptom is in hand and the next step is unclear, or when a document about GC predates the JDK in production.