Garbage collection
You never call malloc. You never call free. In JavaScript, memory shows up when you create a value and disappears when the engine decides you can no longer touch it. Every primitive, every object, every function you write lives somewhere in memory, and something quietly cleans up after you.
That “something” is the garbage collector. This article is about the one rule it follows to decide what stays and what goes.
The question memory management answers
Allocating memory is easy: the moment you write { name: "Maya" }, the engine finds room for that object. The hard part is the other end. When is an object safe to throw away?
Get that wrong in either direction and you have a bug:
- Free too early, and code that still uses the object reads garbage or crashes.
- Free too late (or never), and memory grows until the program slows down or dies. That is a memory leak.
Languages like C hand this decision to you. JavaScript takes it away. The engine watches your values and reclaims the ones you can no longer reach.
Reachability
Everything hinges on one idea: reachability. A value is reachable if there is some way for your running code to get to it. Reachable values are guaranteed to stay in memory. Everything else is fair game for collection.
Reachability starts from a fixed set of values that are always considered “live.” These are the roots:
- The currently executing function, plus its local variables and parameters.
- Every other function in the current chain of nested calls, with their locals and parameters.
- Global variables.
- A few internal engine values you never see.
Roots are reachable by definition. Nobody references them into existence; they are the starting points.
From there the rule extends outward: any value reachable from a root through a reference, or a chain of references, is also reachable. If a global variable holds an object, that object is reachable. If that object has a property pointing at a second object, the second one is reachable too. Follow the arrows as far as they go.
A background process called the garbage collector keeps watch over every object. When an object stops being reachable, it becomes garbage, and the collector frees its memory.
A simple example
Start with one object and one variable pointing at it:
// user holds a reference to the object
let user = {
name: "Maya"
};
The arrow is a reference. The global variable user points at the object { name: "Maya" } — call it “Maya” for short. The name property stores a primitive string, so it is drawn inside the object rather than as a separate arrow.
Right now Maya is reachable: there is a direct path from the user root to it.
Now overwrite that variable:
user = null;
The reference is gone. Nothing points at Maya anymore, and there is no way to reach it from any root. Maya is unreachable. The collector will reclaim its memory, and the string "Maya" inside it goes with it.
Two references
References are values you can copy. Point a second variable at the same object:
// user holds a reference to the object
let user = {
name: "Maya"
};
let admin = user;
Now two arrows land on Maya: one from user, one from admin. Copying the variable did not copy the object — both names refer to the same object in memory.
Clear one of them:
user = null;
Maya survives. The admin variable still references it, so there is still a path from a root. The object stays in memory. Only after you also do admin = null does Maya become unreachable and eligible for collection.
Try it: the object survives as long as either variable still points at it. Only when both are null does it become garbage.
Interlinked objects
Objects can reference each other, which makes reachability more interesting. Here is a small pair of collaborators:
function pairUp(a, b) {
b.partner = a;
a.partner = b;
return {
lead: a,
second: b
};
}
let team = pairUp(
{ name: "Maya" },
{ name: "Priya" }
);
pairUp links two objects to each other — a.partner points at the second, b.partner points at the first — then returns a third object that references both. The result is a small web:
At this point every object is reachable. Trace it: team is a global root; it points at Maya (lead) and Priya (second); Maya and Priya also point at each other. Four incoming paths, all alive.
Now cut two of the references:
delete team.lead;
delete team.second.partner;
Why both? Deleting just one is not enough. Suppose you only ran delete team.lead. Maya would lose that arrow, but Priya still holds partner → Maya, and Priya is reachable through team.second. So Maya stays reachable through Priya. To isolate Maya you have to remove every incoming reference: the one from team and the one from Priya.
With both gone, look at what points at Maya now:
Nothing. Maya still has an outgoing reference (she still points at Priya), but that does not count. Only incoming references keep an object alive. An object pointing at the whole world is still garbage if nothing points back at it.
from a root or a
reachable object
no matter how many
outgoing refs it has
So Maya is unreachable and gets collected, along with the data inside her. After collection:
Unreachable island
Here is the part that trips people up. A whole cluster of objects can reference each other perfectly well and still all be garbage together.
Start from the same linked pair, then drop the one variable that anchors it:
team = null;
Maya and Priya still point at each other. Each one has an incoming reference — from the other. By local inspection, “does someone point at me?”, both look alive. But that misses the point of reachability.
The team object was the only thing tying this cluster to a root. Cut that link and the entire island floats free. There is no path from any root into the group. Two objects referencing each other in a vacuum are still unreachable, so the collector removes the whole island — team, Maya, and Priya — in one go.
This is the difference between “referenced” and “reachable.” Referenced is a local property of one object. Reachable is a global property of the graph: can you get here starting from a root? Only the second one keeps memory alive.
Play with the whole graph below. Toggle any reference on or off, and the model re-runs the same reachability walk the collector uses — starting from the root and following live arrows. Notice that cutting root → team collects the entire island at once, even while Maya and Priya still point at each other.
Inside the collector: mark-and-sweep
The classic algorithm behind all this is called mark-and-sweep. When the collector runs, it does roughly this:
- Take the roots and mark them (remember them as reachable).
- Visit every reference held by those roots, and mark each object it lands on.
- Visit those newly marked objects and mark their references too. Already-marked objects are skipped, so nothing is visited twice and cycles don’t cause infinite loops.
- Keep going until there is nothing new to reach.
- Sweep: everything that was never marked is unreachable, so free it.
Picture a structure like this, with an obvious unreachable island on the right:
Mark the roots first:
Follow their references and mark what they point at:
Keep following references as far as they lead:
Anything the walk never reached is now known to be garbage and gets removed:
A nice mental image: pour a bucket of paint into the roots and let it flow along every reference. Every object the paint touches survives. The dry ones get swept away.
That is the core idea. Real engines wrap it in optimizations so it stays fast and doesn’t freeze your program mid-frame.
Optimizations engines actually use
- Generational collection. Objects get split into “new” and “old” generations. Most objects die young — created, used, discarded within a few operations — so the collector scans the young generation often and reclaims it cheaply. Objects that survive several rounds are promoted to “old” and checked far less frequently, since long-lived objects tend to keep living.
- Incremental collection. Walking millions of objects in one pass would cause a visible pause. Instead the engine breaks the work into pieces and collects a bit at a time. You pay for it with extra bookkeeping to track changes between chunks, but you trade one long freeze for many tiny ones.
- Idle-time collection. Where possible, the collector schedules its work while the CPU is idle — for example between animation frames — to stay out of the way of running code.
You can’t control the timing
One consequence worth sitting with: garbage collection is not something you invoke. There is no reliable gc() you can call, no way to force a collection at a chosen moment, and no way to stop one from happening. The engine decides when, based on allocation pressure and heuristics you don’t see.
So your job is not to manage collection. It is to manage reachability. You never free an object; you make it unreachable, and the collector does the rest. In practice that means dropping references you no longer need:
let cache = { bigData: loadHugeThing() };
// later, when you're done with it:
cache = null; // now the huge thing can be collected
Summary
- Memory is managed automatically. You cannot force collection or prevent it; you influence it only by controlling which values stay reachable.
- An object is kept in memory as long as it is reachable — meaning there is a path to it from a root (globals, current-call locals, and a few internal values).
- Being referenced is not the same as being reachable. A group of objects that reference each other can still be collected as a whole if nothing outside the group connects it to a root. That is the “unreachable island.”
- Only incoming references make an object reachable. Outgoing references never keep an object alive.
- The base algorithm is mark-and-sweep: mark everything reachable from the roots, sweep away the rest. Real engines layer on generational, incremental, and idle-time strategies to keep it fast.
If you want to go deeper, the book The Garbage Collection Handbook: The Art of Automatic Memory Management (R. Jones et al) covers the theory in depth. For V8 specifically, A tour of V8: Garbage Collection is a solid read, and the V8 blog posts about memory-management changes over time. Approaches in other engines are broadly similar but differ in the details.
In-depth engine knowledge pays off when you’re doing low-level optimization. It’s a reasonable next step once the language itself feels comfortable.