V8 · Garbage collection

Inside the collector

A generational collector, on the assumption that most objects die young: a copying scavenger for the nursery, mark-compact for everything that outlives it, and as much of both as possible pushed onto background threads so the main thread keeps running.

01

Two generations

the hypothesis

Almost all objects die almost immediately, so the heap is split: a small new space where everything is born, and a large old space for whatever survives. Allocation in new space is a pointer bump and a bounds check. The bet is that the nursery is mostly garbage by the time anyone looks.

new space  →  (survives twice)  →  old space
02

Scavenger

the young collector

New space is two semi-spaces. A collection copies the live objects out of one and into the other, then declares the whole first half free. Cost is proportional to what survived, not to what was allocated — the dead are never visited at all, which is why a nursery full of garbage is nearly free to collect.

from-space → to-space  (copy the living)
03

Mark-Compact

the old collector

Copying does not scale to a large heap, so old space is marked from the roots, swept into free lists, and compacted by evacuating the most fragmented pages and fixing up every pointer into them. This is the major GC, and the pause people actually notice.

mark → sweep → evacuate fragmented pages
04

Concurrent marking

the pause budget

Most of the marking runs on background threads while JavaScript keeps executing. Tri-colour marking makes that safe: white unreached, grey queued, black finished. The mutator is not allowed to hide a white object behind a black one without telling the collector.

white → grey → black   (never black → white)
05

Write barriers

the tax

Every store of a pointer into an object runs a small piece of extra code. It records old-to-new references into a remembered set, so a young collection never has to scan the whole old heap, and it re-greys black objects during concurrent marking. Reference assignment is not free anywhere in a managed language.

obj.field = other   // → remembered set
06

Handles & roots

the C++ contract

A moving collector invalidates raw pointers, so embedder and runtime C++ never holds one across an allocation. Values live behind Local<T> inside a HandleScope that the GC scans as a root, or behind Persistent when they must outlive the scope. Get this wrong and the bug surfaces a thousand allocations later.

HandleScope scope(isolate);
Local<Object> obj = …;   // safe to hold