Skip to main content

Overview

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 garbage collection

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.

How marking works

The marking phase uses graph traversal to identify live objects:
// Conceptual marking algorithm
function mark(root) {
  const worklist = [root];
  const marked = new Set();
  
  while (worklist.length > 0) {
    const obj = worklist.pop();
    
    if (marked.has(obj)) continue;
    marked.add(obj);
    
    // Add all referenced objects to worklist
    for (const ref of getReferences(obj)) {
      worklist.push(ref);
    }
  }
  
  return marked;
}

Sweep implementation

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 collection

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.

Young generation (nursery)

New objects are allocated in the young generation, which is small and collected frequently.Characteristics:
  • Fast allocation (bump-pointer allocator)
  • Frequent collection (minor GC)
  • Low pause times (small heap region)
  • High mortality rate (90%+ objects die young)
Collection process:
1

Scavenge

Copy live objects to a survivor space using Cheney’s algorithm
2

Promotion

Objects that survive multiple collections are promoted to old generation
3

Reclaim

Entire young generation space is reclaimed and reset

Incremental and concurrent GC

To reduce pause times, modern engines break GC work into smaller increments and perform collection concurrently with JavaScript execution.

Incremental marking

Instead of marking the entire heap at once, incremental GC interleaves small marking steps with JavaScript execution.
1

Start marking

Begin marking from roots in a small time slice (1-5ms)
2

Resume execution

Let JavaScript run while maintaining marking progress
3

Continue marking

Resume marking in subsequent time slices
4

Finish marking

Complete marking when the object graph is fully traversed
// Incremental marking maintains state across pauses
class IncrementalMarker {
  constructor() {
    this.worklist = [];
    this.marked = new Set();
    this.bytesPerIncrement = 64 * 1024; // 64KB
  }
  
  startMarking(roots) {
    this.worklist = [...roots];
  }
  
  // Called periodically
  performIncrementalStep() {
    const startTime = Date.now();
    const timeLimit = 5; // 5ms budget
    
    while (this.worklist.length > 0 && Date.now() - startTime < timeLimit) {
      const obj = this.worklist.pop();
      if (this.marked.has(obj)) continue;
      
      this.marked.add(obj);
      this.worklist.push(...getReferences(obj));
    }
    
    return this.worklist.length === 0; // Done?
  }
}
Incremental GC requires tri-color marking (white/gray/black) and write barriers to track objects modified during JavaScript execution between marking increments.

Concurrent marking

Concurrent GC performs marking on separate threads while JavaScript continues executing on the main thread. Benefits:
  • Main thread pause times reduced to microseconds
  • Marking throughput increased with parallelism
  • Better utilization of multi-core CPUs
Challenges:
  • Requires thread-safe data structures
  • Needs synchronization for object graph mutations
  • More complex write barrier implementation
Main Thread              GC Thread 1           GC Thread 2
-----------              -----------           -----------
JS Execution             Concurrent            Concurrent
     |                   Marking               Marking
     |                      |                     |
     v                      v                     v
[Write Barrier] -----> [Shared State] <----- [Shared State]
     |                      |                     |
     v                      v                     v
JS Execution             Continue              Continue
     |                   Marking               Marking
     v                      |                     |
Short Pause              Complete              Complete
(finalization)              |                     |

WeakMap and WeakSet implementation

Weak collections hold references that don’t prevent garbage collection, enabling memory-conscious caching and metadata storage patterns.

How weak references work

Weak references are invisible to the garbage collector. An object is collectible if only weak references point to it.
// Strong reference - prevents GC
const strongMap = new Map();
let obj = { data: 'important' };
strongMap.set(obj, 'metadata');
obj = null; // Object NOT collected (strongMap still references it)

// Weak reference - allows GC
const weakMap = new WeakMap();
let obj2 = { data: 'important' };
weakMap.set(obj2, 'metadata');
obj2 = null; // Object CAN be collected (weakMap doesn't prevent GC)

WeakMap use cases

// Store private data without preventing GC
const 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

Implementation details

Ephemeron tables: WeakMap/WeakSet use ephemeron tables, where key liveness determines entry liveness.
// Simplified ephemeron table marking
void markEphemerons(EphemeronTable* table) {
  bool changed = true;
  
  while (changed) {
    changed = false;
    
    for (Entry& entry : table->entries) {
      // If key is marked, mark value too
      if (is_marked(entry.key) && !is_marked(entry.value)) {
        mark(entry.value);
        changed = true;
      }
    }
  }
  
  // Remove entries with unmarked keys
  table->entries.removeIf([](Entry& e) { return !is_marked(e.key); });
}
WeakMap keys must be objects. Primitives cannot be weak keys because they don’t have identity that can be collected.

Memory profiling and leak detection

Identifying and fixing memory issues requires understanding profiling tools and common leak patterns.

Using heap snapshots

1

Capture baseline

Take a heap snapshot at application start or after initialization
2

Exercise application

Perform actions that might leak memory (create/destroy components, navigate)
3

Capture comparison

Take another snapshot and compare to baseline
4

Analyze growth

Look for object types that grew unexpectedly (should have been collected)

Common memory leak patterns

// LEAK: Listener not removed
class Component {
  constructor() {
    window.addEventListener('resize', this.handleResize);
    // Component instance leaks when removed from DOM
  }
  
  handleResize() {
    // this.component reference keeps component alive
  }
}

// FIX: Clean up listeners
class Component {
  constructor() {
    this.handleResize = this.handleResize.bind(this);
    window.addEventListener('resize', this.handleResize);
  }
  
  destroy() {
    window.removeEventListener('resize', this.handleResize);
  }
}

Measuring GC impact

// Track GC pauses
if (performance.measureUserAgentSpecificMemory) {
  const baseline = await performance.measureUserAgentSpecificMemory();
  
  // Perform operations
  await heavyOperation();
  
  const after = await performance.measureUserAgentSpecificMemory();
  
  console.log('Memory delta:', {
    bytes: after.bytes - baseline.bytes,
    breakdown: after.breakdown
  });
}

// Monitor GC timing (Chrome DevTools Protocol)
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'measure') {
      console.log(`GC pause: ${entry.duration}ms`);
    }
  }
});
observer.observe({ entryTypes: ['measure'] });
Target GC pause times: < 16ms for 60fps applications, < 100ms for perceived smoothness. Use incremental/concurrent GC to stay within budget.

Next steps

Event loop

Learn how JavaScript schedules asynchronous work with microtasks and macrotasks

JIT optimization

Explore how engines optimize hot code paths with just-in-time compilation

Build docs developers (and LLMs) love