Empirical studies of real programs found the same pattern again and again: the overwhelming majority of allocated objects become garbage within milliseconds, while a small minority stick around for the life of the program. Generational garbage collectors are built entirely around that one observation. Split the heap into a young generation that is swept constantly and cheaply, and an old generation that is swept rarely, and you collect most garbage by scanning only a small fraction of the heap. The mechanism that makes this safe, not just fast, is the more interesting engineering story.
How it works
The generational hypothesis says most objects die young and the ones that survive tend to keep surviving. A generational collector acts on both halves of that claim:
Allocate → Young generation (nursery)
↓ survives one collection
Still young, but tagged with an age counter
↓ survives N collections (the tenuring threshold)
Promoted → Old generation
↓ collected rarely, often concurrently
Collecting the young generation alone is cheap for two reasons: it is small, so scanning it is fast, and most of what is in it is already dead, so a copying collector can evacuate the handful of survivors and treat the rest of the space as immediately free, with no per-object bookkeeping required.
Write barriers and the cost of correctness
Scanning only the young generation is fast, but it is only correct if the collector can find every reference into the young generation, including references held by old-generation objects that were allocated long ago and never re-scanned. Without a way to track those, a young-generation-only scan would miss live objects and free them incorrectly.
The fix is a write barrier: a small piece of code the runtime inserts on every pointer write that crosses from an old object to a young one. Instead of re-scanning the entire old generation on every minor collection, the write barrier records just the old-generation locations that point into the young generation, in a structure called a remembered set, or the coarser-grained card table variant. A minor collection then only needs to scan the young generation plus this small record, not the whole heap.
This is the actual cost of generational collection: every pointer write pays a small write-barrier tax, in exchange for minor collections that scan a tiny fraction of memory. That trade favors most real, allocation-heavy programs.
Tuning knobs and their failure modes
| Runtime |
Young-gen size knob |
Promotion knob |
Effect of raising it |
| JVM (G1 / Parallel) |
-Xmn or region count |
-XX:MaxTenuringThreshold |
Fewer, cheaper minor GCs; more memory held by short-lived objects |
| V8 (Node.js) |
--max-semi-space-size |
Internal, not user-tunable |
Fewer scavenges; higher memory floor per isolate |
| .NET |
GCgen0size |
Internal, adaptive |
Fewer Gen0 collections; higher working set |
Two failure modes show up when this tuning is off. Premature promotion happens when the young generation is too small or a burst of allocation is too fast: objects get promoted to the old generation before they actually die, because the collector could not wait long enough to see them become garbage, and the old generation then fills with short-lived objects that a full collection has to clean up the expensive way. Floating garbage is the opposite tradeoff, made deliberately by concurrent old-generation collectors for lower pause times: an object that died moments after a concurrent scan started may be treated as still live until the next cycle, which is by design, not a bug.
Go's collector sits outside this entire table because it is a concurrent, non-generational, non-compacting mark-and-sweep collector, and that is a considered design choice, not a missing feature. Go leans on aggressive compiler escape analysis to keep short-lived values on the stack, avoiding heap allocation, and therefore the young-generation churn, for a large share of the objects a generational collector exists to handle cheaply. Where Java or JavaScript route short-lived objects through a nursery, idiomatic Go code routes many of the same values through the stack instead, shrinking the problem rather than solving it differently.
Common mistakes
Assuming every managed runtime uses a generational collector. Go's does not, deliberately. Reasoning about Go's GC behavior using Java or V8 mental models about young and old generations will mislead you.
Sizing the young generation without measuring the allocation rate. A young generation that is too small promotes objects prematurely; one that is too large delays minor collections and increases pause size when they do happen. Profile the actual allocation rate before picking a number.
Ignoring write barrier cost in allocation-light, pointer-write-heavy code. Workloads that mutate many object references without allocating much can pay real write-barrier overhead for a generational benefit they barely use.
Treating floating garbage as a leak. A concurrent old-generation collector intentionally lags behind reality by design; a small amount of temporarily "still counted as live" garbage is expected, not a bug to chase.
FAQ
Why do most objects die young in real programs?
Loop temporaries, intermediate calculation results, and short-lived request-scoped objects vastly outnumber long-lived state like caches and connection pools in most workloads. This is an empirical pattern, confirmed repeatedly across languages, not a theoretical assumption.
What is a card table?
A coarse-grained alternative to a per-pointer remembered set: the heap is divided into fixed-size regions, and the collector marks an entire region dirty if any pointer inside it was written, trading some scanning precision for a much cheaper write barrier.
Does a generational collector ever scan the whole heap?
Yes. A full or major collection still scans everything, including the old generation, and is typically triggered by old-generation growth pressure. Generational design reduces how often that expensive scan is needed, not whether it ever happens.
Is Go's non-generational GC a disadvantage?
Not by itself. Combined with escape analysis reducing heap allocation in the first place, it is a coherent alternative design, not a missing feature. Go trades generational complexity for a compiler that avoids allocating short-lived objects on the heap at all.
Where to go next
See memory leaks in JavaScript for 2026 for what happens when objects survive far longer than any generation expects, and Go vs Java in 2026 for how this exact generational-versus-not design choice plays into a real language decision. Immutability in practice for 2026 also connects here, since immutable data shifts allocation patterns in ways that interact directly with how a generational nursery fills up.