18 — Memory Management — Allocation, Reachability, and the Garbage Collector
"The language handles it for me" was my memory model, which made leaks and freezes feel like mysteries. The idea that everything else hangs off: *memory in JavaScript has a three-stage lifecycle — allocate, use, release — and release is automatic, driven by one rule: the garbage collector frees anything that is no longer reachable from a root.* [1]
The framing that finally landed is the reachability view. Unlike C, where I'd manually malloc and free, JavaScript allocates when I create a value and frees it when nothing can reach it anymore. "Reachable" means: starting from a set of roots (global variables, the current call stack, held event listeners), can I follow references and arrive at this object? If yes, it stays. If no — if it's an orphaned cluster with no path back to a root — the collector sweeps it. Memory leaks are not the GC failing; they're me keeping a path alive that I meant to drop.
The three stages
- Allocate — happens implicitly the moment I create a value [1]. const obj = { … } reserves memory for the object. const arr = [1,2,3] reserves memory for the array. I never call an allocation function; the engine does it for me as values come into being.
- Use — the read/write phase. The program reads properties, calls methods, mutates the object. This is just normal execution.
- Release — when the object is no longer reachable, the garbage collector reclaims its memory automatically [1][2]. I never call a free function.
The part that gave me a false sense of security for years was stage 3: because release is automatic, I assumed I didn't need to think about it. The catch is that automatic doesn't mean magic — it means "driven by reachability," and reachability is something I influence with every reference I hold.
Reachability and the garbage collector
The garbage collector's model is reachability [2]. It periodically traces from the roots — global variables, the current call stack, registered event listeners, closures in active use — and marks everything it can reach. Anything unmarked is garbage, and its memory is freed.
The two collection strategies the engines use are mark-and-sweep (trace from roots, mark reachable, sweep the rest) and, for short-lived objects, generational collection (new objects get collected frequently; long-lived ones get checked less often, since most garbage is short-lived). I don't control which runs when; I just control what's reachable.
How leaks actually happen
The insight that made leaks make sense: a leak isn't the GC failing to free memory — it's me keeping a reference path alive that I intended to drop. The object is still reachable, just from somewhere I forgot about. The common patterns [3]:
- Unremoved event listeners. A listener holds a reference to its handler (and anything the handler closes over). If the element it's attached to is removed from the DOM but the listener isn't detached, both stay alive.
- Lingering closures. A closure keeps its captured variables alive. If I cache a closure in a long-lived structure and forget to clear it, everything it closes over stays.
- Global accumulation. Pushing into a module-level array or map and never removing. The structure is reachable from a root, so everything in it is too.
- Detached DOM nodes. Keeping a JS reference to an element I removed from the document. The element is unreachable from the DOM tree but reachable from my variable, so it lingers.
In every case the fix is the same: explicitly drop the reference (removeEventListener, delete, set to null, clear the structure). The GC isn't broken; I just didn't sever the path.
How I use this
The discipline is to think in terms of reference lifetimes, not "freeing." When I add an event listener, I plan the matching removeEventListener. When I cache something in a long-lived structure, I plan how it gets evicted (a Map with size limits, a WeakMap keyed by an object that itself has a natural lifetime). For caches keyed by objects that may disappear, WeakMap and WeakSet are the purpose-built tools — they hold their keys weakly, so the entry vanishes automatically when the key is collected, no manual cleanup needed. And when a memory issue is suspected, the browser DevTools heap snapshot is how I actually see what's holding on — I take two snapshots, find the delta, and look at the retainers. The reachability picture is what tells me what to look for in that snapshot: not "what's the GC missing," but "what reference path did I fail to cut."
References
[1] Mozilla, "Memory management in JavaScript," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management
[2] I. Kantor, "Garbage collection," The Modern JavaScript Tutorial, 2024. [Online]. Available: https://javascript.info/garbage-collection
[3] DebugBear, "Debugging JavaScript memory leaks," 2022. [Online]. Available: https://www.debugbear.com/blog/debugging-javascript-memory-leaks
[4] Medium / Coding Blocks, "Catching memory leaks with Chrome DevTools," 2021. [Online]. Available: https://medium.com/coding-blocks/catching-memory-leaks-with-chrome-devtools-57b03acb6bb9
Knowledge check · Question 1 of 5
In JavaScript, when is memory allocated?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!