gc-fundamentals · git:20260902.3e297ca · 2026-09-02 · sha256 1bddcb77ee73e114

gc-fundamentals git:20260902.3e297caA

Immutable. This exact content never changes and is served at /api/v1/blob/1bddcb77ee73e114.

---
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, not what was allocated.** Copying cost is proportional to
   survivors — to the objects visited more than to their bytes. Collecting a large Eden
   with 99% mortality costs about the same as collecting a small one.
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
   collector is not the problem — go to `-Xlog:safepoint`. On 25 the causes are bulk
   operations with no poll (a large `arraycopy`, zeroing a large array), native code and
   CPU starvation, not counted loops.
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.
- Cost follows survivors, counted in objects. Executed on Temurin 25.0.3: 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 the same 240 ms.
  Smaller objects buy fewer collections, not shorter ones.
- 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, and G1's scheme (SATB pre-barrier plus a queued
  post-barrier) is substantially more expensive than the classic one through 25 — 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 buy
  sub-millisecond pauses with barriers, concurrent CPU and heap headroom for the cycle to
  win its race with allocation. For batch work with no latency SLO, Parallel delivers more
  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.
- Young pauses do not grow with heap size under any generational collector — they grow
  with survivors and with the old-to-young references that must be scanned. What grows
  with the heap is a stop-the-world full collection, and only ZGC and Shenandoah remove
  that dependency.
- Full GC should be zero in a healthy production system. 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.
- **From JDK 27, G1 is the default in every environment (JEP 523).** Through 26 a JVM
  that sees one CPU picks Serial (executed on 25.0.3: `-XX:ActiveProcessorCount=1` logs
  `Using Serial`), so a one-CPU pod runs a stop-the-world collector until the upgrade
  changes it under them.

## 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 JDK 9–28 collector timeline
  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.