AlgoInsight

Free lesson · JavaScript Basic to Advance

Memory and Garbage Collection

Easy5 min read

How JavaScript Manages Memory

JavaScript manages memory automatically. You don't malloc and free like in C, you don't new and delete like in older C++, you don't manually reference-count like in Objective-C without ARC. Instead, the engine's garbage collector reclaims memory automatically when objects are no longer reachable from your code.

function example() {
  const data = { name: "Olena", scores: [85, 90, 95] };
  return data.name;
}

example();   // 'data' becomes unreachable after function returns
// Garbage collector will eventually reclaim its memory

When example returns, no reference to data exists anywhere. The garbage collector eventually notices this and frees the memory data was using.

This is convenient but not free. Garbage collection has costs — time spent collecting, pauses in execution, memory overhead. Understanding how it works is essential for writing performant code at scale.

I'm Emeka. Topics 13.4-13.6 are my section. At Netflix, my team handles 8 billion events per day through Node.js processes. At that scale, memory management isn't an abstract concern — it's the difference between a service that stays up and one that runs out of memory under load. The principles here come from real production experience, not just textbook theory.

The Concept of Reachability

The fundamental rule: an object is alive if it's reachable from a root.

Roots are starting points the garbage collector knows about — typically the global object, the current call stack, and any built-in references. Anything reachable from these roots, through any chain of references, is alive. Anything not reachable is garbage.

let user = { name: "Olena" };   // 'user' is a root reference (top-level variable)
let profile = { user };          // profile.user references the same object

console.log(user.name);
// Object { name: "Olena" } is reachable via 'user' and via 'profile.user'

user = null;
// Still reachable via 'profile.user'

profile = null;
// Now no references — eligible for GC

After user = null and profile = null, nothing references the user object. It becomes garbage. The GC will eventually reclaim its memory.

This is the model. References keep objects alive. Removing the last reference makes them eligible for collection. The GC's job is to find unreachable objects and reclaim them.

Interactive animation: Only what a root reaches stays alive - two objects that reference only each other are still collected.

V8's Generational Hypothesis

V8's garbage collector (called Orinoco) uses generational garbage collection based on an empirical observation: most objects die young.

Run almost any JavaScript program and trace object lifetimes. The vast majority of objects allocated never live more than a few milliseconds. They're temporary variables, intermediate computations, callback scope variables — created, used briefly, discarded. A small minority live longer — application state, caches, long-lived data structures.

This pattern suggests an optimization: collect newly-created objects frequently (most will be dead), collect long-lived objects rarely (most will still be alive). V8 implements this by splitting the heap into two main regions:

The young generation (new space). Where newly-allocated objects start. Small (typically 1-8 MB). Garbage collected frequently and quickly using a copying collector called Scavenger.

The old generation (old space). Where objects that survive young generation collection get promoted. Larger. Garbage collected less frequently using a more expensive Mark-Sweep-Compact collector.

graph LR
    A[New allocation] --> B[Young generation<br/>small, fast GC]
    B -->|survives multiple<br/>young-gen GCs| C[Old generation<br/>large, slow GC]
    B -->|garbage| D[Collected]
    C -->|garbage| D

    style B fill:#10b981,color:#fff
    style C fill:#3b82f6,color:#fff
    style D fill:#dc2626,color:#fff
This diagram shows V8's generational garbage collection. Newly allocated objects start in the young generation, a small memory region that's collected frequently and quickly. Most objects die in the young generation — they're never reachable from anywhere after a brief moment. Objects that survive multiple young-generation collections get promoted to the old generation, a larger region collected less frequently with more expensive algorithms. The hypothesis is that objects which have lived a while will continue living, so collecting them often is wasteful.

The result: young-generation collection runs often but completes in a millisecond or two. Old-generation collection runs rarely but can take 50-200ms in larger applications. Most of your program's allocation activity is in the young generation; that's where the GC focus is.

The Scavenger: How Young-Generation Collection Works

The young generation uses a copying collector. The young generation is split in half: one side is active, the other is empty.

When the active side fills up:

  1. The collector walks reachable objects starting from roots.
  2. For each reachable object found, it's copied to the empty side.
  3. After all reachable objects are copied, the active side is now garbage (all the unreachable objects).
  4. The two sides swap roles. The previously-empty side becomes the active one.

This is fast for two reasons. First, the collector only touches live objects — it never explicitly visits the dead ones. Second, after collection, all live objects are packed together at the start of the new active side. There's no fragmentation; new allocations just append.

The cost is paying twice the memory for the young generation. Half the space is always empty. But the young generation is small relative to total heap size, so this overhead is acceptable for the speed gain.

After an object survives a few young-generation collections (typically two), the engine promotes it to the old generation. The thinking: it's clearly not short-lived, stop wasting time copying it.

Interactive animation: The heap, split by age: survivors of two scavenges leave new space and are promoted to old space.

The Old Generation: Mark-Sweep-Compact

The old generation uses a different algorithm because objects there are larger, longer-lived, and there are more of them. Copying everything would be too expensive.

Mark. Walk from roots, marking every reachable object. Anything not marked is garbage.

Sweep. Walk through old-generation memory, adding unmarked regions to a free list. The marked objects stay where they are.

Compact. Periodically, defragment the old generation by moving objects to eliminate gaps. Without compaction, old-generation memory becomes increasingly fragmented over time.

These steps don't all happen on every collection. V8 uses incremental and concurrent techniques to spread the work across many small pauses instead of one big pause. This is important because long GC pauses freeze your program — bad for user-facing applications and unacceptable for high-throughput servers.

Real V8 GC has many additional optimizations — write barriers, remembered sets, idle-time tasks. The fundamentals are mark, sweep, and compact. The details make those fundamentals practical at scale.

What This Means in Practice

A few practical implications:

Allocations are cheap; collections are expensive. Allocating an object is essentially free — V8 just bumps a pointer in the young generation. The cost comes later, when the GC has to identify whether the object is still alive.

Allocation pressure matters more than peak memory. A program that allocates 10MB and frees 10MB many times per second pressures the GC much more than a program that allocates 100MB once and holds it.

Long-lived caches grow the old generation. A cache that accumulates many entries forces frequent old-generation GCs. Even if individual entries are small, their aggregate cost in GC time can be substantial.

GC pauses are part of your latency budget. For latency-sensitive code (game loops, real-time data, animation), GC pauses can manifest as frame drops or stutters. Reducing allocations on hot paths reduces GC pressure.

At Netflix, we've seen real production issues from each of these. A function that allocated a small object per event, called billions of times per day, produced enough GC pressure to drop our event processing rate by 15%. The fix was reusing object pools instead of allocating fresh per event. That's the kind of optimization the GC model points you toward.

Interactive animation: Per-frame allocation vs reuse: a fresh object for every particle each frame, against mutating them in place.

Weak References

Sometimes you want to reference an object without keeping it alive. JavaScript provides WeakMap, WeakSet, and (more recently) WeakRef for this.

const cache = new WeakMap();
let user = { name: "Olena" };

cache.set(user, { lastSeen: Date.now() });
console.log(cache.get(user));   // { lastSeen: ... }

user = null;   // remove the only strong reference
// The WeakMap entry can be collected
// Eventually, cache will not have the entry

A regular Map would keep the key alive. A WeakMap holds the key weakly — if no other references to the key exist, the GC can collect the key and the entry disappears from the WeakMap.

This is useful for caches that should automatically clean up when their associated objects go away. Don't hold onto entries when the underlying object is gone.

Limitations:

For most "I need a cache" cases, a regular Map with explicit eviction (LRU, TTL) is clearer than WeakMap. Use WeakMap when you specifically want lifetime tied to another object.

Common Pitfalls

Warning: The memory model traps.

Assuming GC is free. Allocations cost CPU time during collection. High allocation rates produce high GC overhead. For hot code paths, allocating less means running faster.

Holding references in long-lived objects. A cache, an event handler, an array — anything long-lived that references shorter-lived objects keeps them alive. We cover specific patterns in Topic 13.5.

Confusing memory leaks with GC pressure. A leak is when memory grows unboundedly. GC pressure is when too much time is spent collecting. They're different problems with different fixes.

Trying to force garbage collection. There's no portable way to trigger GC from JavaScript code. Node has --expose-gc for development, but production code can't rely on it. The engine decides when to collect.

Why This Matters in Interviews

Interview: Memory and GC questions are staff-level territory. Common questions: "How does V8's garbage collector work?", "What's the generational hypothesis?", "What's the difference between Map and WeakMap?". Topic 13.7 covers the interview-prep treatment.

Try It

Exercise: Run Node.js with GC tracing:

node --trace-gc script.js

Write a script that allocates many short-lived objects and observe the GC output:

function allocate() {
  const arr = [];
  for (let i = 0; i < 1_000_000; i++) {
    arr.push({ value: i, label: `item-${i}` });
  }
  return arr.length;
}

for (let i = 0; i < 100; i++) {
  allocate();
}

You'll see Scavenge (young-generation) and Mark-Sweep (old-generation) events. Notice how many young-generation collections happen and how relatively fast they are. Each "Scavenge" line corresponds to one young-generation collection. Now you've seen the GC actually running.

Back to JavaScript Basic to Advance