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.
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:
// 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",
},
};| Space | Contents | GC Strategy |
|---|---|---|
| New Space | Freshly allocated objects | Scavenge (copying GC) |
| Old Space | Long-lived objects | Mark-Sweep-Compact |
| Large Object Space | Objects > ~500 KB | Mark-Sweep (never moved) |
| Code Space | JIT-compiled machine code | Separate management |
| Map Space | Hidden classes/shapes | Collected with old space |
Young Generation: Scavenge
The scavenger uses a semi-space copying algorithm:
// 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
// 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 pagesOrinoco: Concurrent and Parallel GC
V8's Orinoco GC pipeline minimizes main-thread pauses:
// 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",
},
};| Technique | Phase | Main Thread Impact |
|---|---|---|
| Incremental marking | Mark (old gen) | Small interleaved pauses |
| Concurrent marking | Mark (old gen) | Background; minimal pause |
| Concurrent sweeping | Sweep (old gen) | Zero pause |
| Parallel scavenge | Scavenge (young gen) | Shorter pause via parallelism |
| Parallel compaction | Compact (old gen) | Shorter pause via parallelism |
Write Barriers
When old-space objects reference young-space objects, V8 needs to know during scavenge:
// 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 spacePromotion and Tenure
// 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:
// 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
// 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
// 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
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
Frequently Asked Questions
How large is V8's young generation?
What triggers a major GC in V8?
Can I force garbage collection in V8?
How does V8 GC interact with async/await?
What is the maximum heap size in V8?
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.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.