Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 21

Memory Model and Garbage Collection

This chapter assumes you are comfortable with closures, objects, prototypes, and asynchronous code from earlier chapters. You should also be familiar with the distinction between primitives and reference values.
Level: AdvancedLength: In depth4 min read

Read at another depth

Memory Model and Garbage Collection

This chapter assumes you are comfortable with closures, objects, prototypes, and asynchronous code from earlier chapters. You should also be familiar with the distinction between primitives and reference values.

The Heap, the Stack, and Reference Semantics

JavaScript's execution model separates two storage areas. The stack holds the currently executing frames: local primitive values, and pointers to heap objects. The heap holds everything with dynamic lifetime — objects, arrays, functions, closures, and strings. Primitives live on the stack when they are locals, but once stored in an object, array, or closure, they live in heap-allocated structures too.

Assignment never copies objects; it copies the reference.

Code example in javascript
const a = { items: [1, 2, 3] };
const b = a;            // same object, not a copy
b.items.push(4);
console.log(a.items.length); // 4

Closure variables are not stored on the stack per call. When an inner function outlives its creating call, the engine allocates a Context (V8 terminology) on the heap holding the captured variables. This is why the counter in the closures chapter keeps incrementing: count lives in a heap-allocated environment reachable from the returned function.

Reachability: The Only Liveness Rule

Garbage collection is defined by reachability, not reference counting. Starting from roots — the global object, the currently executing stack, active microtask/task queues, and debugger handles — the engine performs a graph traversal. Anything not reachable is garbage, regardless of how many dead cycles point at it.

caption: Reachable objects form a tree from the roots; detached cycles are collected.

alt: A diagram showing the global root pointing to a window state object, which points to a node object, which points to a closure environment holding a count variable. A separate cycle of objects is drawn dashed because nothing links it to the roots, so the collector can free it.

Cycles are not a problem:

Code example in javascript
let a = {};
let b = {};
a.partner = b;
b.partner = a;   // cycle
a = null;
b = null;        // both are collected: unreachable from roots

How Collectors Actually Work

Implementations use generational, tracing collectors. V8's model is representative:

  • New space (young generation): small, scavenged frequently by a fast semi-space copy (Scavenger). New objects allocate here; survivors are promoted to old space. Most objects die young, so this is cheap per byte reclaimed.
  • Old space (old generation): collected by major Mark-Compact cycles. Marking traces from roots, then live objects are either swept or compacted to eliminate fragmentation.
  • Incremental and concurrent phases: marking is interleaved with program execution in small steps ("write barriers" record pointer changes during marking), and large parts run on background threads to avoid long pauses.

Map and Set hold strong references to keys. WeakMap and WeakSet let keys be collected when nothing else references them — essential for attaching metadata to objects without pinning them:

Code example in javascript
const metadata = new WeakMap();
const node = {};
metadata.set(node, { visitedAt: Date.now() });
// when `node` becomes unreachable, its entry vanishes automatically

WeakRefs and FinalizationRegistry exist for interop (caches, native handles), but finalizers run nondeterministically and after collection — never use them for correctness.

Retention: The Practical Failure Mode

GC bugs in real programs are almost never "the collector failed to free a cycle." They are unintended retention: a reachable structure accidentally pins a large object graph. The collector is doing its job; the graph is reachable, so it is not garbage.

Common retention patterns:

  1. Forgotten timers and intervals holding closures over large scopes.
  2. Long-lived caches keyed by objects (Map, not WeakMap).
  3. Event listeners on long-lived DOM elements whose handlers close over removed views.
  4. Detached DOM subtrees referenced from JavaScript — the entire subtree is retained.
  5. Array push without removal in module-level or static state (memoization arrays, logs).
Code example in javascript
const cache = new Map();          // module-level, lives forever
function process(bigObject) {
  cache.set(bigObject.id, bigObject); // retains every object forever
  return compute(bigObject);
}

Fix: bound the cache (LRU with a max size) or use a WeakMap keyed by the object itself.

Detecting Retention

  • Chrome DevTools Memory panel: take a heap snapshot, exercise the suspected leak (open/close a view repeatedly), take a second snapshot, and use the "comparison" view to find objects that grew. The Retainers pane shows the exact chain of references keeping an object alive.
  • performance.measureUserAgentSpecificMemory() (or the older performance.memory) gives coarse heap size signals in production.
  • Allocation timelines show per-function allocation rates; a rising sawtooth that never drops indicates retention.

Decision Criteria: Designing for Collection

  • Prefer WeakMap/WeakSet/WeakRef when the lifetime of auxiliary data should match the lifetime of the key.
  • Explicitly null out module-level references and clearInterval/removeEventListener in teardown paths — engines cannot infer intent.
  • Avoid holding objects inside long-lived closures unless the captured state is needed; narrow the closure to capture only required variables.
  • Do not micro-optimize allocation for the collector. Short-lived allocations are nearly free in generational collectors; retaining is expensive, allocating is not.

Exercise

Two objects reference each other in a cycle and no other code references either. Why does V8 collect them, and would the same be true in a naive reference-counting collector?

Show answer

V8's tracing collector only frees unreachable graphs, and this cycle is unreachable from the roots, so it is collected. A naive reference-counting collector would fail: each object's count stays at 1 due to the mutual reference, so neither reaches zero and both leak.

Exercise

You swap cache.set(bigObject.id, bigObject) for const cache = new WeakMap(); cache.set(bigObject, result). What changes about lifetime, and what is the trade-off?

Show answer

Entries vanish automatically once bigObject becomes unreachable, so no unbounded retention. Trade-offs: keys must be objects (no primitive keys), you cannot enumerate the cache or enforce a size limit, and you lose lookup by id unless you keep the object reference yourself.

Spotted a mistake, or code that does not work?

Report an issue

Chapter: Memory Model and Garbage Collection

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that