JavaScript memory and GC
V8 uses a generational mark-and-sweep GC with concurrent marking. Young gen is scavenged in milliseconds; old gen survives multiple cycles and is collected incrementally.
V8 manages memory automatically with a generational garbage collector called Orinoco. You never call free. You also never have full control. Understanding the model is how you keep your app from hitting "Allocation failed - JavaScript heap out of memory" at 3am.
The generational hypothesis
Most objects die young. A function allocates a few temporary objects, returns, and they are unreachable. Some objects (caches, long-lived state) survive for the lifetime of the page. V8 splits the heap to optimize for both cases.
- Young generation (~1-8MB): two semi-spaces (from-space, to-space). Allocations bump-pointer into to-space. When it fills, the scavenger copies live objects to from-space. Survivors past two scavenges get promoted to old gen.
- Old generation (sized by
--max-old-space-size, default ~1.4GB on 64-bit): mark-compact GC. Marks live objects via tri-color algorithm, sweeps dead, compacts to reduce fragmentation.
Scavenge: cheap and frequent
Young-gen GC (scavenge) takes 1-5ms. It pauses the main thread briefly. Because most objects die young, the live set is small and copying is fast.
Mark-compact: expensive and rare
Old-gen GC happens when the old gen fills. Marking walks every reachable object from the roots. With concurrent marking, most of this work runs on background threads while JS keeps executing. The final compaction step requires a stop-the-world pause of 10-100ms.
Roots
GC starts from roots: the call stack, global scope, the DOM, currently active timers, and a few internal V8 references. Anything reachable from a root is alive. Anything not is dead.
Common leaks
- Global variables holding caches that never evict.
- Event listeners on DOM nodes that get removed but never
removeEventListener-ed (the listener pins the node and its closure). - Timers that close over large state and never
clearInterval. - Detached DOM nodes still referenced from JS state.
- Maps that use objects as keys (use WeakMap if the key should be eligible for GC).
let cache = new Map();
function remember(user) {
cache.set(user.id, user); // grows forever
}
// Fix: use Map + size cap, or WeakMap keyed on the object itselfTools
Chrome DevTools Memory tab gives you heap snapshots and allocation timelines. Diff two snapshots taken before and after an action to find what stuck around. The retainer path tells you exactly which reference is keeping the object alive.
The mental model: you do not control when GC runs, but you do control what is reachable. Make things unreachable and GC will reclaim them. Keep things reachable and they live forever.
Learn more
- Article
- Article
- Docs