JavaScript Garbage Collection Complete Guide
A complete guide to JavaScript garbage collection. Covers reachability, mark-and-sweep, reference counting, generational collection, memory lifecycle, WeakRef, FinalizationRegistry, common GC pitfalls, and how GC interacts with closures, event loops, and DOM references.
JavaScript automatically manages memory through garbage collection (GC). The engine allocates memory when objects are created and reclaims it when objects become unreachable. Understanding how GC works helps you write code that avoids memory leaks and interacts efficiently with the memory system.
For the V8-specific implementation, see How V8 Garbage Collector Works in JavaScript.
Memory Lifecycle
Every value in JavaScript goes through three phases:
// 1. ALLOCATION: Engine allocates memory
const user = { name: "Alice", scores: [95, 87, 92] }; // Object + array allocated
const greeting = "Hello, world!"; // String allocated
const buffer = new ArrayBuffer(1024); // 1KB buffer allocated
// 2. USE: Read and write the values
console.log(user.name);
user.scores.push(88);
// 3. RELEASE: When no references remain, GC reclaims memory
// (happens automatically when objects become unreachable)| Phase | What Happens | Who Controls It |
|---|---|---|
| Allocation | Memory reserved for the value | Engine (automatic) |
| Use | Value is read, written, passed around | Your code |
| Release | Memory returned to the system | Garbage collector (automatic) |
Reachability
The core concept of GC is reachability. An object is reachable if it can be accessed from a root through any chain of references:
// Roots: global object, current call stack, active closures
// Reachable: connected to a root
let data = { value: 42 }; // root -> data
let ref = data; // root -> data (two paths)
// Still reachable after removing one reference
data = null; // ref still points to the object
// Unreachable: no path from any root
ref = null; // Object { value: 42 } is now unreachable
// GC will reclaim it
// Circular references: still collected if unreachable from roots
function createCycle() {
const a = {};
const b = {};
a.ref = b;
b.ref = a;
// When this function returns, both a and b are unreachable
// Modern GC handles circular references correctly
}
createCycle(); // Both objects will be collectedMark-and-Sweep Algorithm
Modern JavaScript engines use mark-and-sweep as the foundational GC algorithm:
// Conceptual illustration of mark-and-sweep
function markAndSweep(roots, heap) {
// MARK PHASE: Start from roots, mark everything reachable
const marked = new Set();
function mark(obj) {
if (!obj || marked.has(obj)) return;
marked.add(obj);
// Traverse all references from this object
for (const ref of getReferences(obj)) {
mark(ref);
}
}
// Mark from every root
for (const root of roots) {
mark(root);
}
// SWEEP PHASE: Free everything not marked
for (const obj of heap) {
if (!marked.has(obj)) {
free(obj); // Reclaim memory
}
}
}
// Roots include:
// - Global variables (window, globalThis)
// - Local variables in the current call stack
// - Closures referenced by active functions
// - DOM elements in the document tree
// - Active timers (setTimeout/setInterval callbacks)
// - Promise callbacks in the microtask queueReference Counting vs Mark-and-Sweep
| Feature | Reference Counting | Mark-and-Sweep |
|---|---|---|
| Mechanism | Count references to each object | Trace reachable objects from roots |
| Circular references | Cannot collect (leak) | Collected when unreachable from roots |
| Collection timing | Immediate when count hits 0 | Periodic or triggered by allocation |
| Overhead | Per-reference increment/decrement | Pause during mark phase |
| Used in JS engines | Legacy (IE6) | All modern engines |
// Reference counting FAILS with circular references
function referenceCounting() {
const a = { name: "A" }; // refCount: 1
const b = { name: "B" }; // refCount: 1
a.partner = b; // b refCount: 2
b.partner = a; // a refCount: 2
// Remove external references
// a refCount: 1 (b.partner still points to it)
// b refCount: 1 (a.partner still points to it)
// Neither reaches 0 -> LEAK in reference counting
// Mark-and-sweep handles this correctly
}Generational Collection
Modern engines divide the heap into generations based on object age:
// Young Generation (Nursery/New Space)
// - Newly created objects land here
// - Collected frequently with a fast "scavenge" algorithm
// - Most objects die young (90%+)
function processRequest(data) {
const temp = data.map((x) => x * 2); // Short-lived: young gen
const result = temp.reduce((a, b) => a + b, 0);
return result;
// `temp` array becomes unreachable, collected in next young gen GC
}
// Old Generation (Old Space/Tenured Space)
// - Objects that survive multiple young gen collections are "promoted"
// - Collected less frequently with full mark-and-sweep
// - Contains long-lived objects: caches, configurations, singleton instances
const appConfig = { // Long-lived: promoted to old gen
theme: "dark",
language: "en",
features: ["search", "notifications"],
};
// This matters for performance:
// - Avoid creating many long-lived temporary objects
// - Short-lived objects are cheap (young gen collection is fast)
// - Old gen collection (major GC) causes longer pausesWeakRef and FinalizationRegistry
These APIs let you interact with GC behavior without preventing collection:
// WeakRef: a reference that does not prevent GC
class ImageCache {
constructor() {
this.cache = new Map();
}
set(url, imageData) {
this.cache.set(url, new WeakRef(imageData));
}
get(url) {
const ref = this.cache.get(url);
if (!ref) return null;
const data = ref.deref();
if (data === undefined) {
// Object was garbage collected
this.cache.delete(url);
return null;
}
return data;
}
}
// FinalizationRegistry: run cleanup when objects are collected
const registry = new FinalizationRegistry((metadata) => {
console.log(`Object collected: ${metadata.label}`);
// Clean up external resources (file handles, network connections)
if (metadata.cleanup) {
metadata.cleanup();
}
});
function createResource(label) {
const resource = {
data: new ArrayBuffer(1024 * 1024),
label,
};
registry.register(resource, {
label,
cleanup: () => console.log(`Cleanup for ${label}`),
});
return resource;
}
let res = createResource("buffer-1");
res = null; // After GC runs, FinalizationRegistry callback firesGC and Closures
// Closures retain their entire scope chain
function outer() {
const largeArray = new Array(1000000).fill("data");
const smallValue = 42;
// This closure captures the entire scope of `outer`
// Both `largeArray` AND `smallValue` are retained
return function inner() {
return smallValue;
};
}
const fn = outer();
// `largeArray` is retained even though `inner` only uses `smallValue`
// (Some engines optimize this, but it is not guaranteed)
// Best practice: extract only needed values
function outerOptimized() {
const largeArray = new Array(1000000).fill("data");
const smallValue = computeResult(largeArray);
// `largeArray` is not referenced by the returned function
return function inner() {
return smallValue;
};
}GC and the Event Loop
// GC pauses happen between event loop tasks
// The engine chooses when to run based on memory pressure
// Incremental GC: breaks work into small chunks between tasks
// This reduces noticeable pauses
// You can hint that you are idle (though not standard):
if ("requestIdleCallback" in window) {
requestIdleCallback((deadline) => {
// Browser may schedule GC during idle periods
while (deadline.timeRemaining() > 5) {
processNextItem();
}
});
}
// Avoid creating pressure: batch allocations instead of scattering them
// BAD: allocates inside a hot loop
function processItemsBad(items) {
for (const item of items) {
const temp = { ...item, processed: true }; // New object each iteration
saveToDB(temp);
}
}
// BETTER: reuse or pre-allocate
function processItemsBetter(items) {
const temp = {};
for (const item of items) {
Object.assign(temp, item);
temp.processed = true;
saveToDB(temp);
// Clear for next iteration
for (const key of Object.keys(temp)) {
delete temp[key];
}
}
}GC Performance Tips
| Tip | Reason |
|---|---|
| Avoid creating objects in hot loops | Reduces young generation pressure and scavenge frequency |
| Null out references when done | Makes objects unreachable sooner for collection |
| Use object pools for frequent allocations | Reuses memory instead of allocating/collecting repeatedly |
| Prefer flat data structures | Fewer references to trace during mark phase |
| Use WeakMap for DOM-keyed caches | Automatically cleans up when DOM elements are removed |
| Avoid closures over large scopes | Prevents unintentional retention of large objects |
Rune AI
Key Insights
- Reachability determines collection: An object is only collected when no reference chain connects it to a root (globals, stack, active closures, DOM tree)
- Mark-and-sweep handles circular references: Unlike reference counting, tracing GC correctly collects mutually-referencing objects that are unreachable from roots
- Generational GC optimizes for short-lived objects: Most objects die young, so frequent young-generation collection is fast and efficient while old-generation collection is less frequent
- Closures retain entire scope chains: A returned function may keep large arrays alive even if it only accesses a small value from the same scope
- WeakRef and FinalizationRegistry cooperate with GC: Use WeakRef for caches that should not prevent collection, and FinalizationRegistry for cleanup callbacks when objects are finally collected
Frequently Asked Questions
Does JavaScript have manual memory management?
How often does garbage collection run?
Do modern engines still use reference counting?
Does setting a variable to null immediately free memory?
What is the difference between minor GC and major GC?
Conclusion
JavaScript garbage collection uses reachability-based tracing with mark-and-sweep to automatically reclaim memory. Modern engines use generational collection where short-lived objects are collected quickly and long-lived objects are promoted to old space. Write GC-friendly code by minimizing allocations in hot paths, nulling references early, using object pools, and leveraging WeakRef/WeakMap for caches. For V8-specific internals, see How V8 Garbage Collector Works. For the mark-and-sweep algorithm in depth, 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.