When you write const data = new Uint8Array(64 * 1024 * 1024), your browser has just asked the operating system for 64 megabytes of memory. What you do not see is what happens when that variable stops being used: nobody runs a free() or remembers your buffer. That silent work is done by the garbage collector (GC), and understanding how it works explains why your JavaScript application sometimes freezes for an instant and sometimes flies.
Memory does not free itself
In languages like C, the programmer releases memory by hand. In JavaScript that option does not exist: the V8 engine (used by Chrome and Node.js) decides for you when an object has died and reclaims its space. The golden rule is reachability: an object lives as long as it can be reached from the roots (global variables, the call stack, or active closures).
The challenge is twofold. First, find what is dead without looking at everything. Second, do it without blocking your interface. It is a constant balance between precision (never free something that is alive) and latency (never stop the execution thread).
Two generations, two strategies
V8 starts from an empirical observation called the generational hypothesis: most objects die young. The ones that survive the first collections tend to live long. So it divides the heap into two regions with different policies.
The young space fills up fast with freshly created objects. There V8 uses a minor GC based on semi-spaces: it copies the survivors from one semi-space to the other and then empties the first one in one go. It is cheap because almost everything it finds is dead, sweeping hundreds of thousands of objects per millisecond.
The old space holds what survives several passes, after a process of promotion (aging). Here copying would be far too expensive, so the engine uses a different algorithm.
Tri-color marking and sweeping
For the old space, V8 runs a major GC with a theory classic: mark-and-sweep. First it marks the live objects starting from the roots; then it sweeps and frees everything left unmarked.
The modern implementation avoids blocking through tri-color marking, a Dijkstra idea. Each object is white (unvisited), grey (found but its references not yet explored) or black (visited with all its references explored). The algorithm finishes when there are no grey objects left: everything white is garbage.
Because marking comes and goes between phases (incremental) and runs in parallel on other threads (concurrent) while your code keeps executing, a write barrier is essential: a small hook every time the code mutates a reference, so a black object cannot suddenly point to a white one without notifying the collector.
Fragmentation and promotion
Sweeping frees memory but leaves it Swiss-cheese-like: small, unusable holes. To fix that, when fragmentation crosses a threshold V8 triggers a compaction: it moves the survivors together. Moving objects forces updating every reference, and that is where writing matters again.
The engine also pretenures: if it detects that a constructor always produces long-lived objects, it creates them directly in the old space and saves several useless copies. And with WeakMap and WeakSet you can tell the GC “do not count these keys as roots”: if the object disappears, the entry is cleared with it, avoiding classic cache leaks.
Why it matters in the real world
GC interruptions are called stop-the-world pauses. V8 has spent years trimming them (the Orinoco project made virtually all the cost concurrent and incremental), but they still exist: a badly timed pause in a game or an animation is felt as lag. You can measure them with the Performance API in the browser or the --trace-gc flags in Node.js.
Reducing leaks is really about keeping your objects reachable only as long as needed: closing listeners, dropping references to unmounted DOM nodes, and using generators instead of piling up giant arrays. The garbage collector is invisible, but the programmer who knows how their reaper thinks writes applications that never stutter.






