How V8 Garbage Collector Works in JavaScript

A complete guide to how the V8 garbage collector works in JavaScript. Covers young generation scavenge, old generation mark-sweep-compact, Orinoco concurrent/parallel GC, write barriers, memory spaces, incremental marking, idle-time GC, and performance implications for your code.

JavaScriptadvanced
17 min read

V8 (the JavaScript engine in Chrome, Node.js, Deno, and Edge) uses a sophisticated garbage collector called Orinoco that combines generational collection, incremental marking, concurrent sweeping, and parallel compaction to minimize pauses while efficiently reclaiming memory.

For the general GC concepts, see JavaScript Garbage Collection: Complete Guide.

V8 Memory Spaces

V8 divides the heap into several spaces, each optimized for different object lifetimes:

javascriptjavascript
// Conceptual view of V8's heap layout
 
const heapSpaces = {
  newSpace: {
    description: "Young generation: newly allocated objects",
    size: "1-8 MB (configurable)",
    gcAlgorithm: "Scavenge (semi-space copying)",
    frequency: "Very frequent (every few ms under pressure)",
    pauseTime: "Sub-millisecond to ~1ms",
    divided: ["from-space (active)", "to-space (copy target)"],
  },
 
  oldSpace: {
    description: "Old generation: objects promoted from new space",
    gcAlgorithm: "Mark-Sweep-Compact",
    frequency: "Less frequent",
    pauseTime: "10-100ms+ (mitigated by incremental/concurrent)",
  },
 
  largeObjectSpace: {
    description: "Objects larger than a page (~500KB)",
    note: "Never moved, collected by mark-sweep only",
  },
 
  codeSpace: {
    description: "Compiled machine code from JIT (TurboFan/Maglev)",
    note: "Managed separately from data objects",
  },
 
  mapSpace: {
    description: "Hidden classes (Maps/Shapes) used by objects",
    note: "Describes object structure for property access optimization",
  },
};
SpaceContentsGC Strategy
New SpaceFreshly allocated objectsScavenge (copying GC)
Old SpaceLong-lived objectsMark-Sweep-Compact
Large Object SpaceObjects > ~500 KBMark-Sweep (never moved)
Code SpaceJIT-compiled machine codeSeparate management
Map SpaceHidden classes/shapesCollected with old space

Young Generation: Scavenge

The scavenger uses a semi-space copying algorithm:

javascriptjavascript
// Conceptual illustration of V8's scavenge algorithm
 
function scavenge(fromSpace, toSpace) {
  // Step 1: Scan roots for references into from-space
  const worklist = [];
  for (const root of getRoots()) {
    if (isInFromSpace(root.target)) {
      worklist.push(root);
    }
  }
 
  // Step 2: Copy live objects from from-space to to-space
  let toPointer = toSpace.start;
 
  while (worklist.length > 0) {
    const ref = worklist.pop();
    const obj = ref.target;
 
    if (obj.forwardingAddress) {
      // Already copied: update reference to new location
      ref.target = obj.forwardingAddress;
      continue;
    }
 
    if (obj.survivalCount >= 2) {
      // Survived enough scavenges: promote to old space
      const promoted = copyToOldSpace(obj);
      ref.target = promoted;
      obj.forwardingAddress = promoted;
    } else {
      // Copy to to-space
      const copy = copyTo(obj, toPointer);
      toPointer += obj.size;
      copy.survivalCount++;
      ref.target = copy;
      obj.forwardingAddress = copy;
    }
 
    // Add this object's references to the worklist
    for (const childRef of getReferences(ref.target)) {
      if (isInFromSpace(childRef.target)) {
        worklist.push(childRef);
      }
    }
  }
 
  // Step 3: Swap spaces (to-space becomes the new from-space)
  swap(fromSpace, toSpace);
  // Old from-space is now empty and becomes the new to-space
}

The scavenge is fast because:

  • Most young objects are dead (only live ones are copied)
  • New space is small (1-8 MB)
  • Copying compacts memory automatically (no fragmentation)

Old Generation: Mark-Sweep-Compact

javascriptjavascript
// V8's Mark-Sweep-Compact for old generation
 
// PHASE 1: MARK (incremental + concurrent)
// - Start from roots, traverse all reachable objects
// - Uses tri-color marking: white (unvisited), grey (visited, children pending), black (done)
function incrementalMark(timeBudgetMs) {
  const deadline = performance.now() + timeBudgetMs;
 
  while (greyList.length > 0 && performance.now() < deadline) {
    const obj = greyList.pop();
 
    for (const child of getReferences(obj)) {
      if (child.color === "white") {
        child.color = "grey";
        greyList.push(child);
      }
    }
 
    obj.color = "black"; // Fully processed
  }
 
  // If grey list is not empty, continue in next increment
  return greyList.length === 0;
}
 
// PHASE 2: SWEEP (concurrent)
// - Scan heap pages and add unmarked objects to free lists
// - Runs on a background thread (main thread continues executing JS)
 
// PHASE 3: COMPACT (parallel)
// - Move surviving objects to reduce fragmentation
// - Updates all references to moved objects
// - Only needed for heavily fragmented pages

Orinoco: Concurrent and Parallel GC

V8's Orinoco GC pipeline minimizes main-thread pauses:

javascriptjavascript
// V8's GC execution model
 
const gcPipeline = {
  incrementalMarking: {
    description: "Mark phase split into small chunks",
    thread: "Main thread (interleaved with JS execution)",
    pausePerChunk: "< 1ms",
    benefit: "Avoids long pauses by spreading work",
  },
 
  concurrentMarking: {
    description: "Mark phase runs on background threads",
    thread: "Helper threads (parallel to main thread)",
    pauseTime: "Near zero (only brief synchronization pauses)",
    benefit: "Most marking happens without pausing JS",
  },
 
  concurrentSweeping: {
    description: "Free lists built by background threads",
    thread: "Helper threads",
    pauseTime: "Zero (completely off main thread)",
    benefit: "Memory reclamation without any pause",
  },
 
  parallelCompaction: {
    description: "Multiple threads compact memory simultaneously",
    thread: "Helper threads + main thread",
    pauseTime: "Reduced by parallelism",
    benefit: "Faster compaction for fragmented heaps",
  },
 
  parallelScavenge: {
    description: "Young gen scavenge uses multiple threads",
    thread: "Main thread + helper threads",
    pauseTime: "Reduced by 50-70%",
    benefit: "Faster young gen collection",
  },
};
TechniquePhaseMain Thread Impact
Incremental markingMark (old gen)Small interleaved pauses
Concurrent markingMark (old gen)Background; minimal pause
Concurrent sweepingSweep (old gen)Zero pause
Parallel scavengeScavenge (young gen)Shorter pause via parallelism
Parallel compactionCompact (old gen)Shorter pause via parallelism

Write Barriers

When old-space objects reference young-space objects, V8 needs to know during scavenge:

javascriptjavascript
// Write barrier: tracks cross-generation references
 
// When you write: oldObject.child = youngObject
// V8 inserts a write barrier that records this in a "remembered set"
 
// Conceptual write barrier
function writeBarrier(object, field, value) {
  object[field] = value;
 
  if (isInOldSpace(object) && isInNewSpace(value)) {
    // Record this reference so scavenge can find it
    rememberedSet.add({ object, field });
  }
}
 
// This allows scavenge to be efficient:
// - Only scan remembered set + stack roots for new space references
// - No need to scan the entire old space

Promotion and Tenure

javascriptjavascript
// Objects are promoted from young gen to old gen based on survival
 
// V8 promotion heuristics:
// 1. Survived at least one scavenge cycle
// 2. New space is filling up (promotion pressure)
// 3. Object is large enough to warrant direct old-space allocation
 
// Impact on your code:
function efficientAllocation() {
  // SHORT-LIVED: stays in young gen, collected cheaply
  for (let i = 0; i < 1000; i++) {
    const temp = { x: i, y: i * 2 };
    processItem(temp);
    // `temp` dies here, collected in young gen scavenge
  }
 
  // LONG-LIVED: promoted to old gen
  const cache = new Map();
  for (let i = 0; i < 100; i++) {
    cache.set(i, { data: computeExpensive(i) });
  }
  // `cache` and all its entries survive multiple scavenges
  // They get promoted to old space
 
  return cache;
}

Idle-Time GC

V8 schedules GC work during idle periods when the main thread has no tasks:

javascriptjavascript
// V8 uses idle time notifications from the embedder (Chrome/Node.js)
// to schedule non-urgent GC work
 
// Chrome tells V8: "You have X ms of idle time before the next frame"
// V8 uses this to:
// - Complete pending incremental marking steps
// - Perform minor GC (scavenge) if young space pressure is moderate
// - Finalize concurrent sweeping
// - Start background compaction
 
// In Chrome: tied to requestAnimationFrame idle periods
// In Node.js: tied to event loop idle phases
 
// You can help by using requestIdleCallback for heavy work:
function processLargeDataset(items) {
  let index = 0;
 
  function processChunk(deadline) {
    while (index < items.length && deadline.timeRemaining() > 2) {
      processItem(items[index]);
      index++;
    }
 
    if (index < items.length) {
      requestIdleCallback(processChunk);
    }
  }
 
  requestIdleCallback(processChunk);
}

Monitoring V8 GC in Node.js

javascriptjavascript
// Node.js exposes V8 heap statistics
const v8 = require("v8");
 
function getHeapStats() {
  const stats = v8.getHeapStatistics();
 
  return {
    totalHeapMB: (stats.total_heap_size / (1024 * 1024)).toFixed(2),
    usedHeapMB: (stats.used_heap_size / (1024 * 1024)).toFixed(2),
    heapLimitMB: (stats.heap_size_limit / (1024 * 1024)).toFixed(2),
    mallocedMB: (stats.malloced_memory / (1024 * 1024)).toFixed(2),
    externalMB: (stats.external_memory / (1024 * 1024)).toFixed(2),
  };
}
 
// Per-space breakdown
function getSpaceStats() {
  return v8.getHeapSpaceStatistics().map((space) => ({
    name: space.space_name,
    sizeMB: (space.space_size / (1024 * 1024)).toFixed(2),
    usedMB: (space.space_used_size / (1024 * 1024)).toFixed(2),
    available: (space.space_available_size / (1024 * 1024)).toFixed(2),
  }));
}
 
// Monitor GC events (Node.js 16+)
const { PerformanceObserver } = require("perf_hooks");
 
const gcObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`GC: ${entry.detail.kind} - ${entry.duration.toFixed(2)}ms`);
    // kind: 1 = Scavenge, 2 = Mark-Sweep-Compact, 4 = Incremental marking
  }
});
 
gcObserver.observe({ entryTypes: ["gc"] });

Writing V8-Friendly Code

javascriptjavascript
// 1. Keep object shapes consistent (hidden classes)
// GOOD: all objects have the same shape
function createPoint(x, y) {
  return { x, y };
}
 
// BAD: different property orders create different hidden classes
const p1 = { x: 1, y: 2 };
const p2 = { y: 2, x: 1 }; // Different hidden class
 
// 2. Avoid deleting properties (deoptimizes hidden classes)
// BAD:
delete obj.property;
// GOOD:
obj.property = undefined;
 
// 3. Pre-allocate arrays when size is known
// GOOD:
const arr = new Array(1000);
for (let i = 0; i < 1000; i++) arr[i] = i;
 
// BAD: grows dynamically, causing repeated reallocation
const arr2 = [];
for (let i = 0; i < 1000; i++) arr2.push(i);
Rune AI

Rune AI

Key Insights

  • Generational heap with two strategies: Young generation uses fast semi-space scavenge (sub-ms pauses), while old generation uses incremental mark-sweep-compact (longer but split across time)
  • Orinoco minimizes main-thread pauses: Concurrent marking and sweeping run on background threads while JavaScript continues executing on the main thread
  • Write barriers enable efficient scavenge: Cross-generation references from old to young space are tracked in remembered sets so scavenge does not need to scan the entire old space
  • Object shape consistency helps V8 optimize: Creating objects with the same property names in the same order allows V8 to reuse hidden classes, speeding up property access
  • Monitor with PerformanceObserver in Node.js: Observe GC events to measure scavenge and mark-sweep durations, identifying whether your application is spending too much time in garbage collection
RunePowered by Rune AI

Frequently Asked Questions

How large is V8's young generation?

By default, V8 allocates 1-8 MB for the young generation (new space), split into two semi-spaces. The size adjusts based on the application's allocation rate and available memory. In Node.js, you can control it with `--max-semi-space-size` (in MB). Larger young space means less frequent scavenges but longer individual pauses.

What triggers a major GC in V8?

major (old generation) GC is triggered when old space approaches its limit, when allocation in old space fails, or when the engine detects high allocation rates. V8 also uses heuristics based on the ratio of live objects to total heap size from the previous collection cycle.

Can I force garbage collection in V8?

In Node.js, start with `--expose-gc` and call `global.gc()`. In Chrome, use the DevTools Memory tab "Collect garbage" button. These are debugging tools only. Never rely on forced GC in production code as it creates unnecessary pauses. See [How to Find and Fix Memory Leaks in JavaScript](/tutorials/programming-languages/javascript/how-to-find-and-fix-memory-leaks-in-javascript) for proper testing techniques.

How does V8 GC interact with async/await?

sync functions create promise objects and suspend execution state. The suspended state and its captured scope variables remain reachable until the promise settles. V8 can run incremental GC between microtask flushes. Long promise chains keep intermediate values alive until the chain completes, so avoid storing large data in variables across await points.

What is the maximum heap size in V8?

V8 defaults to approximately 1.5 GB on 64-bit systems for Node.js, adjustable with `--max-old-space-size` (in MB). In browsers, the limit varies by platform and available system memory, typically 2-4 GB. Exceeding the limit causes an "out of memory" crash.

Conclusion

V8's Orinoco garbage collector uses generational collection with semi-space scavenge for young objects and incremental/concurrent mark-sweep-compact for old objects. Write barriers track cross-generation references, idle-time scheduling reduces visible pauses, and parallel processing speeds up all phases. Write V8-friendly code by keeping consistent object shapes, avoiding property deletion, and minimizing allocations in hot paths. For the foundational GC theory, see JavaScript Garbage Collection: Complete Guide. For the algorithm details, see Mark-and-Sweep Algorithm in JS.