Memory Model and Garbage Collection — and Proxy and Reflect
Assumes you're fluent with closures, prototypes, and async execution from earlier chapters.
Internals
JavaScript engines allocate values on the stack (locals with known lifetimes) or the heap (objects, closures, anything escaping the frame). Most engines implement generational copying collection: the nursery (young generation) collects frequently and cheaply via Cheney semispace scavenging; survivors promote to the old generation, where an incremental mark-and-sweep with a mark bitmap, write barriers, and concurrent marking phases runs on background threads. Major collections are interleaved with allocation and can pause mutator threads in sub-millisecond slices.
Memory is reachable-ness based, not reference-count based: cycles are collected, but a value is retained as long as any live root can reach it through the heap graph. WeakRef and WeakMap hold values without pinning them.
const cache = new WeakMap();
function meta(obj) {
if (!cache.has(obj)) cache.set(obj, { loaded: Date.now() });
return cache.get(obj);
} // entries vanish once obj is otherwise unreachableTrade offs
- Copy vs. mark: scavenging is fast and cache-friendly but pays twice for long-lived objects via promotion; mark-sweep avoids copying but fragments and costs proportional to live set.
- Incremental vs. stop-the-world: incremental marking keeps frame budgets intact but requires write barriers on every reference store.
WeakReffor caches: avoids leaks, but finalization timing is unspecified — never rely on it for semantics, only optimization.- Proxies for instrumentation: universal interception without touching call sites, at the cost of allocation, indirection, and invariant checks on every trap.
Failure modes
- Accidental retention: long-lived closures, module-level arrays, timers, and detached DOM nodes keep whole object graphs alive. A
setIntervalholdingthisis a classic. - Invisible edges:
WeakMapkeys must be objects — primitives throw — andFinalizationRegistrycallbacks are only a hint, never a guarantee. - Proxy invariants: a
gettrap returning a non-configurable, non-writable target property's actual value mismatches the invariant and throws; anownKeystrap on a non-extensible target must report the target's exact keys. Reflect.getvs. raw access: inside traps, accessingtarget.propdirectly re-triggers the invariant machinery; route throughReflectwith the correctreceiverto preserve prototype lookups (notably getters bound to a subclass receiver).
const logged = new Proxy(new Map(), {
get(target, prop, receiver) {
if (prop === 'set') return (k, v) => { console.log('set', k); return target.set(k, v); };
return Reflect.get(target, prop, receiver);
},
});
logged.set('a', 1);Trade offs (choosing whether to intercept)
- Use
Proxywhen call sites must remain untouched or per-object behavior must be dynamic (ORMs, reactivity systems like Vue's). - Use plain wrapper objects when performance matters — proxies are roughly an order of magnitude slower on hot paths and break identity assumptions (
Array.isArraystill works, butproxy === targetfails). - Use
ReflectalongsideProxyto forward default behavior cleanly; standalone, it replaces scatteredObject/Functionstatics with a uniform, receiver-aware API.
diagram mermaid caption: Reachability: roots, live graph, and a weakly-held value alt: A graph showing global scope and an active closure as roots; objects reachable from roots are live; a node held only by a WeakMap is eligible for collection.
exercise Q: Why does Reflect.get(target, 'prop', receiver) matter inside a get trap, instead of returning target.prop? A: Getters on the target capture this from the receiver. Returning target.prop binds this to target, breaking subclass or proxy-context expectations; Reflect.get forwards the correct receiver and preserves prototype-chain semantics.
exercise Q: You replace obj with new Proxy(obj, handler) but a Set of users deduplicates them wrongly. Why? A: Because proxy !== target: identity-based collections (Set, Map, WeakMap) treat the proxy and its target as distinct values. Route all references through one canonical object.
Spotted a mistake, or code that does not work?