Beyond Code & Karma · JavaScript Internals
JavaScript never frees memory by counting references — it frees by reachability. The collector walks out from the roots, marks everything it can reach, and sweeps the rest. Explained through mark-and-sweep, generational heaps, and the law of release.
Chapter 01 — The Foundation
Automatic memory management asks exactly one thing: could this object ever be used again? It answers structurally. There is a set of GC roots — the global object and every variable currently on the call stack. An object is live if a chain of references leads from some root to it. If no such chain exists, no running code can name it, so it is garbage and its memory can be reclaimed. That is the entire contract; everything else is how to compute it quickly.
The Cosmic Connection
In Vedic thought, an object is born into samsara — the wheel of existence — and stays bound only so long as something still clings to it. Liberation, moksha, is not earned by being destroyed; it arrives the moment no root reaches you. The collector is the dispassionate force that grants this release. A memory leak is the opposite — attachment: a lingering reference that traps an object in samsara long after it should have been free.
"Liberation is not earned by cutting every tie — it comes the moment no root holds you."
Chapter 02 — Show Me The Code
The classic tracing algorithm is two phases over the heap graph. Mark walks from the roots flagging the live; sweep frees whatever the walk never touched. Twenty lines capture the whole idea:
// Tracing GC: liveness = reachability from the roots. function gc(roots, heap) { const live = new Set(); const gray = [...roots]; // the work queue while (gray.length) { // ── MARK ── const obj = gray.pop(); if (live.has(obj)) continue; // already reached live.add(obj); // reached from a root → live for (const ref of obj.refs) gray.push(ref); } for (const obj of heap) // ── SWEEP ── if (!live.has(obj)) free(obj); // unmarked → garbage }
Every collector answers the same question; they differ in how, and in what they get wrong.
BFS/DFS over the reference graph starting at the roots. Every object reached is flagged live. Nothing reached is, by definition, still in use.
Walk the heap. Every object the mark phase missed is unreachable garbage — its memory is reclaimed and returned to the allocator.
Survivors are slid together to close the holes sweeping left, so future allocation is a fast pointer bump. This is what a moving/compacting collector adds.
Chapter 03 — Live Visualization
A tiny heap. One root reaches a → b; an isolated cycle
c ⇄ d and a lone object e have no root.
Run a mark-sweep step and watch the traversal light up the reachable,
then the unreachable dissolve.
Chapter 04 — Deep Dive
This is where the basic explanations stop. Here's what actually matters when you're shipping at scale.
Give every object a counter of incoming references and free it when the counter hits zero. Simple and prompt — and it leaks. Build two objects that point at each other with no root pointing at either: each has a count of one, so a reference counter never frees them, even though nothing in your program can reach them. A tracing collector starts from the roots, never reaches the island, and sweeps it. Reachability sees a truth a local count cannot — which is exactly why real engines trace instead of count.
Decades of measurement gave one robust rule: most objects die young. The temporary array in a loop, the request object, the intermediate string — born and abandoned within milliseconds. A handful live for the whole program. Generational GC exploits this by splitting the heap:
| Generation | Holds | Collected by |
|---|---|---|
| Eden (young) | Freshly allocated objects | Frequent, cheap minor GC |
| Survivor | Objects that survived a collection | Minor GC; promoted on age |
| Tenured (old) | Long-lived survivors | Rare, expensive major GC |
Scanning the small, high-death young space most of the time — and the whole heap seldom — is the trick that keeps real apps responsive.
A minor GC traces only the young generation. It's frequent and fast, reclaiming the many short-lived corpses cheaply while promoting the few survivors. A major GC traces the entire heap including the old generation — far rarer and more expensive. To stay correct, a minor GC also follows a remembered set: the old→young references, so an old object pointing into Eden still keeps its young target alive without scanning all of old space.
A leak in a GC language is not lost memory — it's memory still rooted by accident. The classic culprit is a listener you never remove: its closure pins everything it captured for as long as the target lives.
// A "leak" in a GC language = something still reachable by accident. function mount() { const huge = new Array(1e6).fill(0); // the handler closure captures `huge` — and never lets go window.addEventListener('resize', () => use(huge)); } // mount() returns, but `huge` stays rooted by the live listener // Fix: keep a handle, then drop it so no root reaches the closure. const onResize = () => use(huge); window.addEventListener('resize', onResize); window.removeEventListener('resize', onResize); // now collectable
The same shape recurs everywhere: stray setInterval
callbacks, closures over big data, detached DOM nodes still held in
a variable or array, and ever-growing global caches. The fix is
always identical — drop the reference so no root reaches the object.
A normal Map or array is a strong root: anything it
holds is pinned. When you want to associate data with an object
without keeping that object alive, reach for a
WeakMap — it holds its keys weakly, so the key's
reachability elsewhere decides liveness, and the entry is collected
automatically once the key is unrooted.
// A strong Map is a root: every key it holds is pinned forever. const strong = new Map(); strong.set(node, meta); // `node` can now never be collected // A WeakMap holds its keys weakly — the KEY's reachability decides. const weak = new WeakMap(); weak.set(node, meta); // when `node` is unrooted elsewhere, // this entry is swept automatically
WeakMap is the idiomatic
fix for "cache keyed by DOM node / object that must not leak." You
literally cannot iterate it — because doing so would require
pinning the keys, defeating the point.
The simplest collector is stop-the-world: pause all JS, mark, sweep, resume. Correct, but a long pause janks the frame. Real engines mark incrementally, interleaving small slices of marking with your code, using the tri-color abstraction — white (unreached), gray (reached, children pending), black (done). A write barrier catches the dangerous case where running code points a black object at a white one mid-mark, re-graying it so nothing live is wrongly swept.
Chapter 05 — Dharma & The Heap
Karma is the principle that every action carries a consequence,
settled at the right time. To allocate is to act —
to bring an object into being and bind it to the living program. So
long as a root still holds it, it persists in
samsara.
// Allocation is birth into samsara… let attachment = { held: true }; // rooted — it persists // …release the reference, and no root reaches it. attachment = null; // moksha: the collector grants release
The moment you release the last reference, no root reaches it — and the collector, holding no grudge and playing no favourites, grants moksha. It does not punish a cycle for loving itself; it simply finds no path from the eternal roots and lets go. Even mutual attachment cannot prevent release once nothing clings from the outside.
"Free not by destroying, but by no longer being reached." — the Gita, interpreted for systems engineers 🙏
Chapter 06 — Test Yourself
Three questions. No cheating. Your karma is watching.
The Enlightenment
कर्म करो, फल की चिंता मत करो