JavaScript Garbage Collection: Complete Guide

Garbage collection automatically frees memory your code no longer needs. Learn how reachability, reference counting, and generational collection work across JavaScript engines.

6 min read

JavaScript manages memory automatically through garbage collection. You never call free or delete. When you create objects, arrays, or functions, the engine allocates memory for them.

When those values become unreachable, the garbage collector reclaims the memory.

The core idea is reachability. An object is reachable if it can be accessed from a root.

Roots include global variables, local variables on the call stack, and active closures. If an object is reachable, it stays alive. If the collector determines an object is not reachable from any root, it is garbage and its memory is freed.

How Reachability Works

Reachability is determined by tracing references from roots through the object graph. Every reference from a root to an object counts. Every reference from that object to another object counts.

The collector follows every chain of references and marks everything it finds.

javascriptjavascript
let user = { name: "Alex" };
// user is a root. The object is reachable.
 
user = null;
// The object is now unreachable. GC will collect it.

A more complex example shows how references keep objects alive. Multiple variables can point to the same object, and the object stays reachable as long as at least one reference exists.

javascriptjavascript
let person = { name: "Alex" };
let employee = person;
// Two roots point to the same object.
 
person = null;
// The object is still reachable through employee.
// GC will not collect it.
 
employee = null;
// Now unreachable. GC can collect it.

Objects can reference each other in cycles. Even if a group of objects references only each other and no root reaches any of them, they are all garbage. Modern collectors handle cycles correctly.

For how V8 implements this specifically, see /javascript/how-v8-garbage-collector-works-in-javascript.

Reference Counting vs Tracing

There are two fundamental approaches to garbage collection: reference counting and tracing. JavaScript engines use tracing, but it is worth understanding both.

Reference CountingTracing (Mark-and-Sweep)
How it worksCounts references to each object; collects when the count hits zeroWalks the object graph from roots and marks everything reachable
Handles cyclesNo, two objects referencing only each other never reach zeroYes, an unreachable cycle is simply never marked
Used byOlder systems, some reference-counted languagesEvery modern JavaScript engine

Tracing handles cycles correctly because if no root can reach a cycle, none of its objects get marked. This is why JavaScript engines universally chose tracing over reference counting.

JavaScript engines universally use tracing collectors. For a detailed walkthrough of the algorithm, see /javascript/mark-and-sweep-algorithm-in-js-full-tutorial.

The Generational Hypothesis

All modern JavaScript engines use generational garbage collection. The key insight is that most objects die young. A temporary string created for logging, an intermediate array built during a calculation, a short-lived event object: these are allocated and discarded quickly.

Objects that survive for a while tend to survive much longer. A global configuration object, a long-lived DOM element, or a module-scoped cache may live for the entire lifetime of the page.

Generational collection splits the heap into a young generation (nursery) and an old generation. The nursery is small and collected frequently using a fast copying collector. Objects that survive a few nursery collections are promoted to the old generation, which is collected less frequently.

Minor GC: Collecting the Nursery

The nursery is collected using a copying collector. The space is split into two halves: the active half where new objects are allocated, and the inactive half used during collection.

When the active half fills up, the collector scans all roots and finds which nursery objects are reachable. It copies each live object to the inactive half, compacting them together. Dead objects are simply abandoned.

When copying completes, the halves swap roles.

This is fast because most nursery objects are dead. The collector only spends time on live objects. If the nursery is 16MB and only 2MB of objects are live, copying those 2MB is quick.

The collector also updates all pointers to point to the new locations.

Major GC: Collecting the Old Generation

The old generation is collected less frequently and uses a different algorithm. Mark-and-sweep is the standard approach.

The mark phase starts from roots and traverses every reachable object, setting a mark bit on each one. The sweep phase walks the entire old generation linearly. Every unmarked object is added to a free list for future allocations.

Some collectors add a compact phase after sweeping. Live objects are moved together to eliminate fragmentation. This is more expensive but prevents the heap from becoming a checkerboard of small free chunks.

Major GC pauses can be significant because the old generation is large. Engines use concurrent marking (running the mark phase in a background thread) and incremental marking (splitting the mark phase into small steps) to keep pauses short.

What Allocates Memory

Every value in JavaScript consumes memory. Some allocations are obvious. Others are easy to overlook.

Objects, arrays, and functions are heap-allocated. Each new object literal, array literal, or function expression allocates memory. Closures allocate memory for the captured variables.

Classes allocate memory for the prototype chain.

Strings are immutable in JavaScript, so every string concatenation creates a new string. Template literals with embedded expressions create new strings. Regular expressions create RegExp objects.

Boxed primitives like new Number(42) allocate wrapper objects, though these are rarely used in practice. Symbol values are allocated once and shared.

How to Write GC-Friendly Code

You cannot control the garbage collector, but you can write code that minimizes its workload.

Avoid unnecessary allocations in hot code paths. A loop that runs thousands of times should not create a new object on every iteration. Reuse objects or use primitive values instead.

Be mindful of closures that capture large objects. A closure that captures a multi-megabyte array keeps that array alive as long as the closure exists.

Clean up event listeners, timers, and subscriptions when they are no longer needed. An active interval that references a large data structure prevents that data from ever being collected.

For practical memory leak diagnosis, see /javascript/fixing-javascript-memory-leaks-complete-guide.

Rune AI

Rune AI

Key Insights

  • JavaScript uses automatic garbage collection based on reachability from roots like global objects and the call stack.
  • All modern engines use generational collection: a fast nursery for new objects and a slower old generation for survivors.
  • The mark-and-sweep algorithm traces live objects from roots and frees everything else.
  • GC pauses are short for minor collections and minimized through concurrent techniques for major collections.
  • Minimizing allocations in hot paths and cleaning up references reduces GC pressure.
RunePowered by Rune AI

Frequently Asked Questions

Can I force garbage collection in JavaScript?

In Node.js with the --expose-gc flag, you can call global.gc(). In browsers, there is no direct way to trigger GC. In both cases, manually triggering GC is rarely needed and can hurt performance.

Does garbage collection pause my application?

Yes, but modern engines minimize pause times. Minor GC on the nursery takes a few milliseconds. Major GC uses concurrent and incremental techniques so pauses are usually under 50ms.

Conclusion

Garbage collection is one of JavaScript's greatest conveniences and greatest sources of invisible performance costs. Understanding how reachability, generational collection, and mark-and-sweep work helps you write code that cooperates with the GC rather than fighting it. Minimize allocations in hot paths, clean up references, and let the collector handle the rest.