open atlas
↑ Back to track
JavaScript Engine internals JSE · 06 · 02

Generational GC and the Scavenger

Most objects die young, so V8 splits the heap by age. New space is two semi-spaces collected by the Scavenger using Cheney's copying algorithm: copy survivors, flip the spaces, promote two-time survivors to old space. Orinoco makes it parallel.

JSE Middle ◷ 14 min
Level
FoundationsJuniorMiddleSenior

Profile a hot request handler and you will find it allocates relentlessly: a parsed body, a few intermediate strings, a response object, dozens of temporaries — and drops every one of them before the next request. If the GC had to scan the whole multi-hundred-megabyte heap each time to reclaim that churn, you would feel it. It does not. It bets that the youngest objects are almost all dead, scans only a tiny region, and copies out the handful of survivors. That bet is the most important design decision in the whole collector.

The generational hypothesis

The browser/03-v8-internals/05-gc-orinoco lesson gives the overview; here we open the Scavenger’s mechanics.

The empirical foundation of every production GC is the generational hypothesis: most objects die young. The lifetime distribution of allocations is wildly bimodal — a huge majority are transient (request temporaries, intermediate strings, per-render React elements) and become unreachable within a few milliseconds, while a small minority (caches, the module graph, connection pools) live for the whole process. Almost nothing dies in the middle.

V8 exploits this by splitting the heap by age:

  • New space (young generation) — where every ordinary object is born. Small: a budget that grows on demand up to roughly 1–8 MB per isolate (tunable via --max-semi-space-size). Collected frequently and cheaply by a minor GC.
  • Old space (old generation) — long-lived survivors. Can be hundreds of MB to gigabytes (--max-old-space-size). Collected infrequently by a major GC (next lesson).

Because new space is small and almost everything in it is dead by collection time, a minor GC touches very little live data — that is the entire payoff.

New space is two semi-spaces; allocation is a bump pointer

New space is physically two equal halves called semi-spaces: to-space (active) and from-space (idle). Allocation into to-space is the cheapest operation in the engine — a bump-pointer allocator: keep a pointer to the next free byte, hand it out, advance the pointer by the object size. No free-list search, no fragmentation, just an add and a bounds check.

// Conceptually, every `new`/object literal does:
//   if (top + size > limit) triggerScavenge();
//   addr = top;
//   top += size;          // bump
//   return addr;
const point = { x: 1, y: 2 };   // a bump-pointer allocation in to-space

When the bump pointer hits the limit (to-space is full), a Scavenge (minor GC) fires.

The Scavenger: Cheney’s copying algorithm

The Scavenger is a copying collector running Cheney’s algorithm. The key inversion versus mark-sweep: it does not look at dead objects at all. It copies out the live ones and abandons the rest wholesale.

  1. Flip. The roles swap: the current to-space becomes from-space, the empty half becomes the new to-space.
  2. Evacuate roots. Scan the roots (stack, globals, handles) plus the remembered set of old→new pointers (see lesson 04 — old space may reference young objects, and we are not scanning old space). Copy each referenced live young object from from-space into to-space, leaving a forwarding pointer in the old slot.
  3. Cheney scan. Walk the newly copied objects in to-space breadth-first as a work queue; for each, copy any from-space objects it references (or follow an existing forwarding pointer if already copied), updating the pointer to the new location. This continues until the scan pointer catches the allocation pointer — the queue is empty.
  4. Reclaim implicitly. Whatever was not copied is simply left in from-space, which is now declared empty in one stroke. Dead objects cost nothing to reclaim — they are never visited.

The work is proportional to the live set, not the allocated set. With the generational hypothesis holding, the live set is tiny, so the Scavenge is fast.

Promotion: surviving makes you old

An object that survives a Scavenge is not yet trusted to be long-lived; it is copied into to-space and given another chance. If it survives a second Scavenge, V8 concludes it is probably long-lived and promotes it: copies it into old space instead of back into a semi-space. (V8 also promotes early under “intermediate” generation pressure, and directly allocates very large objects into old/large-object space, but “survive twice → promote” is the model to hold.)

Promotion is why a Scavenge can leave new space nearly empty: transient objects were never copied (dead), and the few real survivors graduated to old space. New space stays small and the next Scavenge stays cheap.

The Scavenger by the numbers
New space (semi-space) size
~1–8 MB
Minor GC (Scavenge) pause
sub-ms to low-ms
Promotion threshold
survive ~2 Scavenges
Allocation cost
bump pointer (~O(1))
Work proportional to
live set, not allocated
Parallel scavenger since
V8 6.2 (2017)

Orinoco: making it parallel

Orinoco is the umbrella name for V8’s modern GC project — the set of techniques that turn a stop-the-world collector into a mostly-concurrent, parallel, incremental one. For the Scavenger specifically, Orinoco made it parallel: multiple helper threads share the copying work via dynamic work-stealing, so wall-clock pause time drops even though there is more raw work. Because new space is small, minor pauses land in the sub-millisecond to low-millisecond range — short enough to fit comfortably inside a 16.6 ms animation frame. The heavier concurrency and incrementality apply mostly to the major GC, which is the next two lessons.

Quiz

A loop allocates 1,000,000 short-lived objects, of which fewer than 100 are still reachable at each Scavenge. Roughly what does the cost of each Scavenge scale with?

Quiz

An object allocated in new space is still reachable after two Scavenges. Where does it end up, and why?

Order the steps

Order one Scavenge (minor GC) cycle.

  1. 1 Bump pointer hits the to-space limit; allocation triggers a Scavenge
  2. 2 Flip: to-space becomes from-space, the empty half becomes the new to-space
  3. 3 Copy roots' and remembered-set's live young objects into to-space, leaving forwarding pointers
  4. 4 Cheney scan: walk copied objects, copy what they reference, update pointers
  5. 5 Promote objects surviving their second Scavenge into old space
  6. 6 Abandon from-space wholesale — dead objects reclaimed implicitly
Edge cases

There is a real cost hidden in “copying is cheap”: a freshly promoted object can carry pointers into now-stale locations, and pointer-heavy survivors make a Scavenge slower than a pure-temporary workload of the same allocation count. The fast path assumes a low survival rate. A workload that allocates and retains a large fraction of new objects (building one giant array, say) defeats the generational bet — survivors get copied repeatedly until promoted, and you see more minor GC time than the allocation count alone predicts.

Recall before you leave
  1. 01
    Walk through one Scavenge (minor GC) cycle in V8.
  2. 02
    Why does a copying collector make short-lived garbage essentially free to reclaim?
  3. 03
    What is the generational hypothesis and how does V8's heap layout exploit it?
Recap

The generational hypothesis — most objects die young — drives V8’s two-region heap. New space is small (~1–8 MB) and holds freshly allocated objects; allocation there is a bump pointer, the cheapest operation in the engine. When it fills, a minor GC (Scavenge) runs Cheney’s copying algorithm: flip the two semi-spaces, copy live young objects from from-space into to-space (leaving forwarding pointers and following the remembered set for old→young references), breadth-first-scan the copies to evacuate what they reference, then abandon from-space wholesale. Because only live objects are ever copied, dead objects cost nothing and the Scavenge scales with the survivor count — tiny by the hypothesis — keeping minor pauses sub-millisecond. Objects surviving two Scavenges are promoted to the large old space, collected separately and rarely by the major GC. Orinoco, V8’s GC project, made the Scavenger parallel via work-stealing across helper threads, dropping wall-clock pause time further. Now when you see minor GC time spiking in a profile, you know to check survival rates: a workload that retains most of what it allocates defeats the generational bet and you can measure that directly with --trace-gc.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done
Connected lessons
appears again in208

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.