The JavaScript engine

Garbage collection

Understand reachability, mark-and-sweep collection, generations, and why collection can reclaim only unreachable values.

In JavaScript you create objects all the time and never free them. There’s no free() call. The engine does that job for you with a garbage collector, the part of V8 that finds values your program can’t reach anymore and reclaims their memory.

The key word is reachable. The collector starts from a set of roots: global variables and the local variables of every function on the call stack. It follows every reference from those roots, then from the objects it finds, and so on. Anything it reaches is alive. Anything it never reaches is garbage.

Let’s see it in practice:

let user = {
  name: 'Ada',
  preferences: { theme: 'dark' }
}

user = null

After user = null, nothing points to that object. The preferences object goes with it, because the only path to it went through user. Both are now eligible for collection.

Notice I said “eligible”, not “freed”. The engine decides when to run the collector. It might run right away, or a few seconds later. The exact strategy changes between browsers and V8 versions.

How modern collectors work

The classic algorithm is mark-and-sweep: mark everything reachable from the roots, then sweep away what’s not marked. V8 adds a few tricks on top.

It splits the heap into generations. Most objects die young, think of a temporary array inside a loop. So V8 keeps new objects in a small “young” space that it collects often and quickly. Survivors move to the “old” space, which is collected less often.

It also does part of the work incrementally and on background threads, so the main thread pauses for short slices instead of one long freeze.

These are optimizations to reduce pauses, not timing guarantees you can build on.

What this means for your code

Three things matter in practice:

  1. Temporary objects still cost work, even if they get collected a moment later. A hot loop that creates thousands of small objects makes the collector run more often.
  2. A reachable value can never be collected, even if your feature is done with it. That’s how memory leaks happen, the topic of the next lesson.
  3. Memory usage doesn’t have to drop the moment you release a reference. Wait for a collection before you conclude something leaked.

You can watch this yourself. In the DevTools Memory panel, take a heap snapshot. Then run this in the Console:

let big = Array.from({ length: 100000 }, (_, i) => ({ id: i }))
big = null

Click the trash-can icon to collect garbage, take a second snapshot, and compare. The 100,000 objects are gone. Remove the big = null line and repeat: now they stay, and the Retainers section shows big holding them.

Don’t try to force garbage collection from application code. There is no standard API for it. Make unwanted objects unreachable, and the engine does the rest.

Lesson completed