Learn how JavaScript engines automatically manage memory through mark-and-sweep collection, generational strategies, and incremental/concurrent algorithms
Garbage collection (GC) is the automatic memory management system that JavaScript engines use to reclaim memory occupied by objects that are no longer reachable. Understanding GC mechanics is essential for writing performant JavaScript applications and diagnosing memory issues.
Modern JavaScript engines use sophisticated GC strategies to minimize pause times while efficiently reclaiming memory. The techniques covered here are implemented in production engines like V8, SpiderMonkey, and JavaScriptCore.
Mark-and-sweep is the fundamental algorithm underlying most JavaScript garbage collectors. It operates in two distinct phases:
1
Mark phase
The collector starts from root objects (global variables, stack references, active closures) and traverses the object graph, marking every reachable object.
2
Sweep phase
The collector scans through the heap, reclaiming memory from any unmarked objects. These objects are unreachable and safe to collect.
After marking completes, the sweep phase reclaims unmarked memory:
// Simplified sweep algorithm (C++ pseudocode)void sweep(Heap* heap) { for (HeapObject* obj : heap->all_objects()) { if (!obj->is_marked()) { heap->deallocate(obj); } else { obj->clear_mark(); // Reset for next GC cycle } }}
Mark-and-sweep requires pausing JavaScript execution during collection. This “stop-the-world” behavior can cause noticeable latency in applications, which is why modern engines use incremental and concurrent techniques.
Generational GC exploits the weak generational hypothesis: most objects die young. By dividing the heap into generations, engines can collect short-lived objects more frequently and efficiently.
Generational collection requires tracking pointers from old to young objects (“remembered sets”).Why they’re needed:When collecting only the young generation, the collector needs to know about old-to-young references to avoid collecting live young objects.
// Old generation objectconst oldObj = { refs: [] };// Young generation objectconst youngObj = { data: 42 };// This assignment requires a write barrieroldObj.refs.push(youngObj);// Engine records this cross-generational reference
Implementation:
// Pseudocode for write barriervoid writeBarrier(Object* old_obj, Object* young_obj) { if (in_old_generation(old_obj) && in_young_generation(young_obj)) { remembered_set.add(old_obj); }}
Incremental GC requires tri-color marking (white/gray/black) and write barriers to track objects modified during JavaScript execution between marking increments.
// Store private data without preventing GCconst privateData = new WeakMap();class User { constructor(name) { this.name = name; privateData.set(this, { password: 'hashed_password', sessionToken: 'token_abc123' }); } getPrivateData() { return privateData.get(this); }}let user = new User('Alice');// When user is collected, private data is automatically removed
// Cache expensive computations without memory leaksconst computationCache = new WeakMap();function expensiveOperation(obj) { if (computationCache.has(obj)) { return computationCache.get(obj); } const result = /* expensive computation */; computationCache.set(obj, result); return result;}// Cache entries are automatically removed when keys are GC'd
// Attach metadata to DOM nodes without leaksconst elementMetadata = new WeakMap();function trackElement(element) { elementMetadata.set(element, { clicks: 0, lastInteraction: Date.now() }); element.addEventListener('click', () => { const meta = elementMetadata.get(element); meta.clicks++; meta.lastInteraction = Date.now(); });}// Metadata automatically cleaned up when elements are removed from DOM
// LEAK: Listener not removedclass Component { constructor() { window.addEventListener('resize', this.handleResize); // Component instance leaks when removed from DOM } handleResize() { // this.component reference keeps component alive }}// FIX: Clean up listenersclass Component { constructor() { this.handleResize = this.handleResize.bind(this); window.addEventListener('resize', this.handleResize); } destroy() { window.removeEventListener('resize', this.handleResize); }}
// LEAK: Closure captures large contextfunction createHandler(largeData) { return function handler() { // Entire largeData is kept alive even if only small part is used console.log(largeData.id); };}// FIX: Extract only needed datafunction createHandler(largeData) { const id = largeData.id; // Copy primitive return function handler() { console.log(id); // Only id is captured, largeData can be GC'd };}