Skip to main content

What is garbage collector in JavaScript?

Garbage collector is a built-in part of the JavaScript engine that automatically finds and frees memory used by objects no longer reachable from active code.

Theory

TL;DR

  • Think of it as a hotel housekeeper: she removes luggage only after confirming no guest holds a claim ticket to it.
  • No references from roots (globals, call stack) = the object is garbage.
  • V8 uses mark-and-sweep: trace everything reachable from roots, then reclaim the rest.
  • Circular references do not cause leaks in modern engines. Mark-and-sweep handles them.
  • You cannot force GC in production. Real leaks come from forgotten timers, detached DOM nodes, and growing globals.

Quick example

javascript
let user = { name: "Alice" }; // memory allocated on the heap console.log(user.name); // "Alice" - object is reachable user = null; // last reference cut // { name: "Alice" } is now unreachable from any root // V8 will reclaim it on the next GC cycle console.log(typeof user); // "object" - null is still a value

After user = null, nothing in the program can reach that object. It becomes a candidate for collection. The engine decides when to actually free the memory.

How mark-and-sweep works

V8 starts from a set of roots: the global object, the current call stack, and active handles. From each root it traces every reference - properties, closures, prototype chains - and marks each reachable object. When the traversal finishes, every unmarked object gets reclaimed.

The key insight is reachability, not reference count. A cycle between two objects means nothing if neither is reachable from a root. Both get collected.

javascript
function createCycle() { let a = {}; let b = {}; a.ref = b; b.ref = a; // cycle created } createCycle(); // a and b go out of scope here // neither is reachable from any root // V8 collects both - no leak

Old reference-counting engines had trouble with exactly this pattern. Mark-and-sweep does not.

Generational GC and incremental marking

V8 splits the heap into two spaces. Young generation holds newly allocated objects, most of which die quickly. V8 cleans this space with fast scavenge passes. Objects that survive a few rounds get promoted to old generation, where full mark-and-sweep runs less often.

Since V8 7.4, marking runs incrementally. The engine pauses the main thread in small slices instead of one long stop-the-world pause. This reduces jank in UI-heavy apps. Node 22+ ships with Orinoco GC, which improves throughput for server workloads.

From JS code you cannot observe any of this directly. But it explains why DevTools sometimes shows memory that looks free yet has not been reclaimed.

Sources of memory leaks

setInterval is the sneakiest leak in production. I once debugged a Node.js service growing 50 MB per hour, and the culprit was an interval inside a request handler that nobody had cleared. GC only collects what it cannot reach, so if you accidentally keep a reference alive, the object stays in memory indefinitely.

Four common sources:

  • Timers: setInterval keeps its callback and every closure it references alive until you call clearInterval.
  • Event listeners: a listener on a DOM element holds a reference to its handler. Remove the element without removing the listener and both stay in memory.
  • Globals: window.cache = [] is a root. Everything pushed into it never gets collected. Use WeakMap for object-keyed caches instead.
  • Detached DOM nodes: removing an element from the DOM tree does not free it if a JS variable still holds a reference.

Common mistakes

Mistake: thinking delete frees memory

javascript
let obj = { prop: "value" }; delete obj.prop; // removes the property key // obj itself is still allocated - heap size unchanged

delete removes a key from an object. To make the object eligible for GC, remove the reference to it: obj = null.

Mistake: storing unbounded state in globals

javascript
window.cache = []; function add() { window.cache.push(new Array(1_000_000)); } // cache is a root, grows forever

Scope the data inside a function or module, or use WeakMap for object-keyed caches so entries are released automatically.

Mistake: forgetting to remove event listeners

javascript
element.addEventListener("click", handler); // element removed from DOM, but handler closure still alive

Call removeEventListener in cleanup code, or use AbortController to cancel multiple listeners at once.

Mistake: expecting immediate collection

javascript
largeObj = null; // memory may still show as used for seconds in DevTools

Setting to null makes the object eligible. GC runs on its own schedule and does not respond to your code. Monitor trends in heap snapshots, not single measurements.

Real-world usage

  • React: useEffect cleanup functions (clearInterval, removeEventListener) prevent closure leaks in dashboards and SPAs.
  • Node.js/Express: WeakMap for per-request caches lets entries be collected when the request object goes out of scope.
  • Lodash: uses WeakMap internally in memoize so cached results are released along with the original object.
  • Chrome DevTools Memory tab: take a heap snapshot before and after an action, then compare the Retained Size column to find what is holding memory.

Follow-up questions

Q: How does V8 detect that an object is unreachable?
A: It starts from roots (global, call stack) and traverses all references. Objects not touched during the traversal are unreachable. That is the mark phase of mark-and-sweep.

Q: Do circular references cause memory leaks in modern JS?
A: No. Mark-and-sweep detects unreachable cycles by checking reachability from roots, not by counting references. This was a real problem for older reference-counting engines but not for V8.

Q: How do you force GC during development?
A: Run Node.js with --expose-gc and call global.gc(). Browsers have no equivalent in production. Chrome DevTools lets you trigger a collection manually from the Memory tab.

Q: What is the difference between young and old generation?
A: Young generation holds short-lived objects and is cleaned with fast scavenge passes. Old generation holds survivors and uses full mark-and-sweep. Most objects never leave young generation, which is why typical apps stay fast.

Q: A Node.js service grows from 300 MB to 2 GB over 24 hours. How do you debug it?
A: Take heap snapshots at intervals and compare the Retained Size column. Look at what is growing - likely an array, Map, or EventEmitter accumulating entries. Suspect setInterval callbacks and any global cache without a size limit. Add process.memoryUsage() to a metrics endpoint to confirm the trend before opening DevTools.

Examples

Basic: breaking a reference

javascript
function allocate() { let data = new Array(100_000).fill("x"); // large heap allocation return data; } let result = allocate(); // result holds the array result = null; // array is now unreachable, eligible for GC

After result = null, the last reference to the array is gone. The 100k-element allocation becomes garbage on the next collection pass.

Intermediate: React cleanup preventing a leak

javascript
function Dashboard() { const [items, setItems] = useState([]); useEffect(() => { const id = setInterval(() => { setItems(prev => [...prev, { ts: Date.now() }]); }, 1000); return () => clearInterval(id); // cleanup on unmount }, []); return <div>{items.length} items loaded</div>; }

Without clearInterval, the interval keeps running after Dashboard unmounts. Each tick adds an entry to items, and the closure holds the component state alive. Heap grows until the page reloads. The cleanup function is what gives GC permission to do its job.

Advanced: WeakMap for metadata without leaking

javascript
const metadata = new WeakMap(); function attachMeta(obj, info) { metadata.set(obj, info); } let request = { id: 42 }; attachMeta(request, { startTime: Date.now() }); request = null; // WeakMap does not prevent GC // when request is collected, its entry in metadata disappears automatically // a regular Map would hold the reference and cause a leak

WeakMap keys are held weakly. When request is set to null and no other reference exists, the object is collected and the corresponding WeakMap entry disappears with it. A regular Map would keep that object alive indefinitely, which is the exact leak pattern the WeakMap was designed to prevent.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?