Day 8: Garbage Collection Mechanics & Memory Leak Detection
Understand mark-and-sweep and generational GC well enough to reason about real leaks in long-running SPAs, and practice finding one in DevTools.
Study
Concepts
Reachability, not reference counting, decides what survives
V8 (and modern engines generally) use a mark-and-sweep algorithm rooted at a set of "roots" (global object, currently executing call stack, closures in scope). The GC marks everything reachable from roots, then sweeps (frees) everything unmarked. An object with zero incoming references is NOT what matters — what matters is whether it is reachable from a root, which is why two objects referencing each other in a cycle can still be correctly collected once nothing else points to either.
V8 splits the heap into a small "young generation" (new objects, collected frequently and cheaply via a fast Scavenge algorithm since most objects die young) and an "old generation" (survivors, collected less often via a more expensive mark-sweep-compact pass). This generational strategy is why allocating many short-lived objects is usually cheap, but is exactly what triggers frequent young-gen GC pauses if overdone in a hot path like a scroll handler.
The classic leak patterns in real apps
Detached DOM nodes: a removed DOM node stays in memory if a JS variable (or a closure) still references it. Forgotten timers/intervals/subscriptions: `setInterval` or an event listener that outlives the component that created it keeps its whole closure scope alive. Global variable accumulation: attaching growing arrays/caches to `window` or a module-level singleton with no eviction policy.
In React specifically: subscribing in `useEffect` without a cleanup function, or capturing stale state in a closure passed to a long-lived listener, are the two most common leak sources.
See It
Visualizations
Visualization
Mark-and-Sweep in one GC cycle
(a "stop-the-world" pause, minimized via incremental/concurrent marking)
walk from roots (globals, stack, closures) and flag every reachable object
reclaim memory for every unmarked object
defragment memory to keep allocation fast
Build It
Code Examples
A real leak: forgotten interval closure
function startPolling(bigDataset) {
// bigDataset stays alive for as long as this interval exists,
// even if the component that called this unmounts.
const id = setInterval(() => {
console.log('still polling with', bigDataset.length, 'rows');
}, 5000);
return id;
}
const id = startPolling(new Array(1_000_000).fill('row'));
// Leak: nothing ever calls clearInterval(id) — bigDataset lives forever.
// Fix: always pair the subscription with teardown
function startPollingFixed(bigDataset) {
const id = setInterval(() => {
console.log('polling', bigDataset.length);
}, 5000);
return () => clearInterval(id); // caller MUST invoke this on cleanup
}React version of the same leak, and the fix
// Leaks: no cleanup, listener outlives the component
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
}, []);
// Fixed: cleanup function removes the listener on unmount
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);Remember
Key Takeaways
- GC frees what is unreachable from roots — reference cycles are collected fine, unlike naive refcounting.
- Young-gen (Scavenge) collections are frequent and cheap; old-gen (mark-sweep-compact) are rarer and pricier.
- The top 3 real-world leaks: detached DOM refs, forgotten timers/listeners, and unbounded global caches.
- In React, every subscription started in useEffect needs a matching cleanup return.
- Use Chrome DevTools Memory tab (heap snapshot diff, or Allocation instrumentation) to find leaks empirically, not by guessing.
Do It
Practice
- 1Take two heap snapshots in DevTools (before/after interacting with a page repeatedly) and compare retained size to find a growing object.
- 2Intentionally write a component that leaks via a missing useEffect cleanup, confirm the leak in the Memory tab, then fix it.
- 3Explain, in your own words, why a detached DOM node still counts as "reachable" if a JS variable references it.