---
title: "18 — Memory Management — Allocation, Reachability, and the Garbage Collector"
uid: memory-management
tags: ["roadmap:javascript", "memory-leaks", "garbage-collection", "performance", "javascript", "memory"]
excerpt: "Memory has a three-stage lifecycle — allocate, use, release — and release is automatic: the garbage collector frees anything no longer reachable from a root."
date: 2026-08-13T03:28:04+0000
source: https://www.aveshina.my.id/en/blog/memory-management
---

"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.

```figure
<svg viewBox="0 0 740 280" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Three stages of the memory lifecycle: Allocate (create a value), Use (read and write during execution), Release (GC frees unreachable memory). A root node at top reaches a green cluster of objects; an orphaned grey cluster with no path back to the root is about to be collected.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- three stages -->
    <rect x="20" y="30" width="200" height="70" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="120" y="55" font-size="13" font-weight="700" fill="#1e1b4b" text-anchor="middle">1. Allocate</text>
    <text x="120" y="75" font-size="10" fill="#475569" text-anchor="middle">implicit, on creation</text>

    <rect x="270" y="30" width="200" height="70" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="370" y="55" font-size="13" font-weight="700" fill="#052e16" text-anchor="middle">2. Use</text>
    <text x="370" y="75" font-size="10" fill="#475569" text-anchor="middle">read / write during run</text>

    <rect x="520" y="30" width="200" height="70" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="620" y="55" font-size="13" font-weight="700" fill="#422006" text-anchor="middle">3. Release</text>
    <text x="620" y="75" font-size="10" fill="#475569" text-anchor="middle">GC frees the unreachable</text>

    <!-- reachability graph -->
    <text x="370" y="135" font-size="11" font-weight="700" fill="#475569" text-anchor="middle">reachability — what the collector actually checks</text>

    <!-- root -->
    <rect x="335" y="150" width="70" height="34" rx="8" fill="#1e1b4b" stroke="#6366f1" stroke-width="1.5"/>
    <text x="370" y="172" font-size="11" font-weight="700" fill="#fff" text-anchor="middle">root</text>

    <!-- reachable cluster -->
    <circle cx="270" cy="225" r="22" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <circle cx="370" cy="245" r="22" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <line x1="355" y1="184" x2="285" y2="208" stroke="#16a34a" stroke-width="1.5"/>
    <line x1="370" y1="184" x2="370" y2="223" stroke="#16a34a" stroke-width="1.5"/>
    <text x="270" y="228" font-size="9" fill="#052e16" text-anchor="middle">reachable</text>

    <!-- orphan cluster -->
    <circle cx="520" cy="225" r="22" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="3,3"/>
    <circle cx="600" cy="245" r="22" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="3,3"/>
    <line x1="540" y1="225" x2="580" y2="245" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="3,3"/>
    <text x="560" y="208" font-size="9" font-style="italic" fill="#94a3b8" text-anchor="middle">unreachable → freed</text>
  </g>
</svg>
```

## 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](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](https://javascript.info/garbage-collection)

[3] DebugBear, "Debugging JavaScript memory leaks," 2022. [Online]. Available: [https://www.debugbear.com/blog/debugging-javascript-memory-leaks](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](https://medium.com/coding-blocks/catching-memory-leaks-with-chrome-devtools-57b03acb6bb9)

```quiz
Q: In JavaScript, when is memory allocated?
- Explicitly, via a function call like malloc()
- Implicitly, the moment a value (object, array, etc.) is created
correct: 1
explain: JS allocates automatically on value creation. There's no malloc/free — the engine reserves memory whenever an object, array, or closure comes into being.

Q: The garbage collector frees an object when…
- a fixed time has passed since it was created
- the object is no longer reachable from any root (no reference path back to a global, the call stack, or a held listener)
correct: 1
explain: Collection is driven by reachability. The GC traces from roots; anything unreachable is freed. "Unreachable" is the only criterion — time-since-creation is irrelevant.

Q: A memory leak in JS is best described as…
- the garbage collector failing to run
- a reference path the developer forgot to sever, keeping an object reachable when it should have been freed
correct: 1
explain: Leaks aren't GC failures. They're live references — an unremoved listener, a cache never cleared, a closure held too long — that keep an object reachable. The GC correctly keeps it, because something can still reach it.

Q: Why are unremoved event listeners a common leak source?
- The listener holds a reference to its handler (and what the handler closes over); if not detached, both stay reachable even after the target element leaves the DOM
- Listeners are automatically freed when the element is removed
correct: 0
explain: A registered listener keeps a reference chain alive. Removing the element from the DOM doesn't detach the listener, so the handler and its closure stay reachable. Always pair addEventListener with removeEventListener.

Q: When should you prefer a WeakMap over a Map?
- When the keys are objects with their own lifetime and the entries should vanish automatically once the key is collected
- When you need ordered iteration of entries
correct: 0
explain: WeakMap holds its keys weakly; when a key object becomes unreachable elsewhere, the entry is eligible for collection with no manual cleanup. Perfect for caches/metadata keyed by objects. Note: WeakMaps are not iterable, so use Map when you need to walk entries.
```
