Concept
The beginner framing: garbage collection (GC) is the automatic process that finds objects your program can no longer reach and reclaims their memory, you never trigger it manually, and you (mostly) can't control exactly when it runs.
The precise mental model: modern JavaScript engines use mark-and-sweep as the core algorithm, which specifically solves a problem that JavaScript's earlier, simpler approach (reference counting) could not.
Why reference counting fails: circular references
An older, simpler GC strategy counts how many references point to each object, freeing it when that count hits zero.
function makeCircle() {
const a = {};
const b = {};
a.ref = b;
b.ref = a; // a and b now reference EACH OTHER
}
makeCircle();
// After makeCircle() returns, NOTHING outside references a or b.
// But under pure reference counting: a's count is 1 (from b.ref), b's count is 1 (from a.ref), // neither ever reaches zero, so neither is EVER collected. A permanent leak.Two objects that reference only each other, with nothing external referencing either, should obviously be collectible, but reference counting can't see that, because it only counts incoming references locally, never asking "is this reachable from somewhere that actually matters?"
Mark-and-sweep: the actual algorithm
- Mark phase: starting from the roots (global scope, active call stack, active closures), the collector traces every reference outward, marking every object it can reach as "alive."
- Sweep phase: anything NOT marked is, by definition, unreachable, the collector reclaims its memory.
This correctly handles the circular reference case above: a and b reference each other, but neither is reachable from any root once makeCircle() returns, so the mark phase never reaches them, and the sweep phase reclaims both.
Generational GC: most objects die young
V8 (and most modern engines) split the heap into two generations, based on a real, empirically observed pattern: the vast majority of objects become garbage almost immediately (a function's local variables, a short-lived intermediate array, a one-off object literal), while a small minority survive for a long time (module-level singletons, cached data, long-lived application state).
- Young generation (the "nursery"): small, collected very frequently, using a fast algorithm ("Scavenger"), cheap because it's a small region and most objects here are already garbage by the time it runs.
- Old generation: objects that survive a few young-generation collections get promoted here, collected far less often, using a more thorough (and more expensive) mark-and-sweep-and-compact pass, since this region is larger and a full scan is costlier.
This two-tier design is a direct performance optimization: it would be wasteful to run an expensive, thorough collection pass across the entire heap every time, when most collectible garbage is concentrated in short-lived, recently-created objects.
WeakRef and FinalizationRegistry: deliberate escape hatches
let cache = new WeakRef({ large: "data" });
console.log(cache.deref()); // { large: "data" }, while the object is still alive elsewhere
// If nothing else references the original object, it CAN be collected, // cache.deref() may then return undefined, at the GC's discretionA normal reference always keeps its target alive. A WeakRef holds a reference that does not count toward reachability, the referenced object can still be collected even while a WeakRef points to it. This is intentionally a rare, advanced tool (most caching needs are better served by WeakMap, covered in Data Structures), the spec explicitly warns against relying on GC timing, since exactly when collection happens is deliberately left unspecified and engine-dependent.
Try It
Reason through which objects survive garbage collection after this code runs, before checking the solution.
let a = { name: "a" };
let b = { name: "b" };
let c = { name: "c" };
a.friend = b;
b.friend = a; // a and b reference each other
b = null; // remove the DIRECT reference to b...
c = null; // c is now completely unreferenced