A tutorial on how garbage collection works in JavaScript, starting from what memory is. Seven short programs whose object graphs a mark-and-sweep collector walks in front of you — a reference cycle collected anyway, a closure quietly holding a million-element array, a WeakMap entry dying with its key, and tri-colour marking interrupted mid-walk — plus the nursery and old generation, a guide to finding a leak in your own code with Chrome DevTools, and the source or command behind every claim.
A frontend app built by Ananda Rizki. More of them at Labs.
Garbage collection in JavaScript
You never free memory. You stop reaching it.
You have never called anything to reserve memory in JavaScript, and you have never called anything to give it back. That is unusual — most languages make you do at least one of the two — and it is worth understanding what is being done on your behalf before looking at how. This page assumes nothing: no C, no computer science course, and none of the words on the rest of this tutorial.
WHAT A LINE OF CODE ACTUALLY DOES
A value lives somewhere
const user = { name: "ada" } does two separate things. It reserves a piece of memory big enough to hold that object and writes the object into it — and then it puts the address of that piece into the variable user. The variable does not contain the object. It contains directions to it.So two variables can mean one object
const other = user copies the directions, not the object. Now two names lead to one piece of memory, and other.name = "grace" changes what user.name reads. This is the whole reason the rest of this page is about a graph: a program's objects are not a list, they are a web of things pointing at each other.Reserving space is easy; giving it back is not
The hard part is that the future is not knowable
user.name again; that question is equivalent to asking whether the program halts. Every memory-management design in existence is therefore a way of approximating it, and the three below are the three approximations anyone has made work.Nothing above is specific to JavaScript. It is true of Python, Java, C and Go alike; the languages differ only in who is made responsible for the last step.
WHAT YOU ACTUALLY CONTROL
There is no free
delete removes a property; it is not C++'s delete.You control the graph, and only the graph
Timing is not observable, on purpose
“Leak” means something different here
Which is a short list, and it is the good news: there is exactly one thing to learn to debug memory in this language, and it is what points at what.
THE THREE DESIGNS — AND WHY JAVASCRIPT USES THE THIRD
01
You free it yourself
The language gives you malloc and free, or new and delete, and you are responsible for pairing them. C, C++ and Rust all live here, in increasing order of how much help you get.
Free too early and every remaining pointer to that memory is now pointing at something else — a use-after-free, the single most exploited class of bug in software. Free too late, or not at all, and you leak. Free twice and you corrupt the allocator. All three are silent at the point of the mistake.
Fast and exact, and it makes correctness your problem on every line.
02
Count the references
Give every object a counter. Bump it when a new reference to the object is made, decrement it when one goes away, and free the object the moment the counter hits zero. Python and Swift use this, and so did Smalltalk before generational collectors arrived.
Two problems, one annoying and one fatal. The annoying one is that every single assignment now costs arithmetic — Ungar measured the total overhead of reference counting in a Smalltalk system at about 20% of CPU time in 1984. The fatal one is that a group of objects pointing at each other holds its own counts above zero forever. Nothing outside can reach them; none of their counters will ever reach zero; the memory is gone for good.
Prompt and simple, and it cannot collect a cycle. Lesson 03 is a nine-line program that this design leaks.
03
Start from what you can still reach
Forget counters. Pick the handful of places the program is guaranteed to still have access to — the global object, the variables in the function calls currently running, a few engine-internal handles — call them the roots, and walk outwards following every reference. Whatever the walk reaches is kept. Whatever it does not reach is freed, and it does not matter how many things were pointing at it.
The walk costs time proportional to the number of live objects, and it has to happen while your program is not editing the graph — which is why the rest of this page is largely about making that affordable. And collection is no longer prompt: an object is not freed when it becomes garbage, but the next time somebody looks.
Cycles stop being a category. This is what every JavaScript engine ships, and MDN says so outright: no modern engine uses reference counting any more.
SO THE WHOLE SUBJECT IS ONE SUBSTITUTION
The collector cannot answer will this object be used again, so it answers can this object still be reached from a root instead, and treats the second as if it were the first. That substitution is wrong in one direction only: it keeps things it did not need to. It never frees something you can still get at.
Every lesson in this tutorial is one consequence of that sentence. A cycle nobody can reach is collected, because reaching is the test. A closure that mentions an array keeps the array, because the mention is a reference and references are the test. A removed DOM node in a cache stays, because the cache is reachable and so the node is. The collector is never wrong; it is answering a slightly different question than the one you had in mind.
Next: the same ideas stated compactly, as something to look things up in.
Simulator
Garbage Collection
A first-principles tutorial on garbage collection in JavaScript. It opens by explaining what memory is, what a reference is, and the three designs anyone has made work — manual free, reference counting, and tracing from roots — so that "reachability, not reference counting" means something before it is asserted. Then seven short programs, each with its object graph drawn out and walked by a real mark-and-sweep collector: a reference cycle that is collected anyway, a closure quietly holding a million-element array, a detached DOM node kept alive by a cache, a Map and a WeakMap that differ by one word, and tri-colour marking interrupted mid-walk to show why the write barrier exists. Every lesson also says why the collector is built that way, what the rule costs you in real code, and how to check the claim yourself. An eighth view drops references entirely and shows the same collector as memory — a nursery, an old generation, promotion after two survivals. A ninth is the part most tutorials skip: how to find a leak in your own application with a heap snapshot, a retainer chain and the three-snapshot technique. A tenth lists the sources: the ECMAScript specification's liveness clause, V8's own posts, Ungar's 1984 paper, and the Node commands that produced the numbers, with their real output. The point is that a leak in JavaScript almost always means "still reachable by accident", and that reachability is a substitute for a question no collector can answer.
Built by Ananda Rizki, a frontend developer — one of the small web apps he writes for fun at Labs.