Module 61 : Memory Management & Garbage Collection —
1) Mental Model of Memory
2.1 Stack vs Heap
Stack: Stores function frames (primitives, references to objects). Grows/shrinks with calls/returns.
Heap: Stores objects, arrays, functions, closures, DOM nodes, buffers.
2.2 Reachability Graph
GC roots include: the current call stack, global object (window/global), active closures, Web APIs tasks (pending timers, promises’ reactions), DOM rooted in the document, and JS engine internals.
An object is collectible when it is no longer reachable from any root.
2.3 References
Strong references: Normal variables, object properties, array elements, Map keys/values.
Weak references: WeakMap, WeakSet, WeakRef. Do not prevent GC.
Key principle: GC reclaims unreachable objects, not merely “unused.” If you keep a reference—intentionally or accidentally—the object stays.
3) Modern Garbage Collection (High Level)
Most modern engines (V8, SpiderMonkey, JSC) use:
Generational GC: New allocations go to a young generation (nursery/new space). Most die young → fast minor collections. Survivors are promoted to old generation.
Mark–Sweep–Compact:
Mark: starting from roots, mark reachable objects (tri-color/incremental marking to reduce pauses).
Sweep: reclaim unmarked memory.
Compact: reduce fragmentation by moving objects and updating references.
Incremental & Concurrent phases: Work is split across small slices and background threads to shorten “stop-the-world” pauses.
Write barriers / card marking: Track mutations during incremental marking so the GC’s view stays correct.
Implications for developers: Many short-lived allocations are cheap; long-lived, ever-growing graphs are the problem.
4) What Causes Memory Leaks in JS?
Accidental globals / module singletons retaining large structures.
Timers/Intervals (setInterval, setTimeout) not cleared.
Event listeners on long-lived targets (e.g., window, document, body) never removed.
Detached DOM trees: Elements removed from the document but still referenced in JS.
Unbounded caches (Map, arrays) that only ever grow; missing eviction policy.
Closures capturing large objects inadvertently.
Observers (MutationObserver, IntersectionObserver, ResizeObserver) not disconnected.
WebSockets/Streams with dangling handlers.
Third-party libraries keeping hidden references (charts, virtual lists).
5) Tooling
5.1 Browser
Memory panel → Heap snapshot:
Compare snapshots: Before action, After action, After cleanup.
Inspect Retainers to see who keeps a reference.
Allocation instrumentation on timeline: Record allocations during user flows; spot cumulative growth.
performance.memory (non-standard): Quick estimates in Chrome.
performance.measureUserAgentSpecificMemory() (chromium-only): Per-iframe/process breakdown.
Performance panel: Correlate GC pauses with scripting activity.
5.2 Node.js
process.memoryUsage(): RSS, heapTotal, heapUsed, external.
v8.getHeapStatistics(): Engine-level stats (via v8 module).
Inspector / DevTools: node --inspect to take heap snapshots in Chrome.
Heap dumps: Use heapdump (development only) to capture .heapsnapshot for offline analysis.
Workflow tip: Always create a repro script/page, then use A/B snapshots with a single change to isolate leaks.
6) Patterns
6.1 Control Lifetime Explicitly
Provide dispose() / destroy() methods to release listeners, timers, observers, and DOM.
Prefer AbortController
const ac = new AbortController();
element.addEventListener('click', handler, { signal: ac.signal });
// later
ac.abort(); // removes listener automatically
For one-off handlers: { once: true }.
6.2 Prefer Block Scopes & Avoid Accidental Globals
Use let/const. Avoid var. Run linters (no-undef, no-global-assign).
6.3 Bounded Caches
Implement LRU or size-based eviction for Map/arrays.
6.4 Use Weak Collections for Ephemeral Keys
For DOM-node keyed metadata, prefer WeakMap so entries vanish when the node is GC’d.
6.5 Beware of Closures
Don’t capture whole objects if you only need a small piece. Extract primitives; pass IDs.
6.6 Clean up Observers & Intervals
Keep references and always disconnect() / unobserve() / clearInterval() in component teardown.
6.7 FinalizationRegistry & WeakRef (Advanced)
Can observe when objects are GC’d or hold weak references.
Caveats: Non-deterministic, timing is unspecified; never use for correctness/security-critical logic.
7) Worked Examples (Leak → Diagnose → Fix)
Example 1: Event Listener Capturing Large Array
Buggy
const big = new Array(1e5).fill('xxxxx');
function attach() {
window.addEventListener('resize', () => {
// Closure captures `big`, keeping it alive forever.
console.log(big[0]);
});
}
attach();
Fixes
// (A) Decouple: only capture what you need
const big = new Array(1e5).fill('xxxxx');
const big0 = big[0];
window.addEventListener('resize', () => console.log(big0), { once: true });
// (B) Or use AbortController to later remove it
const ac = new AbortController();
window.addEventListener('resize', handler, { signal: ac.signal });
// later: ac.abort();
Example 2: Detached DOM Tree
Buggy
let cached;
function build() {
const div = document.createElement('div');
div.textContent = 'temp';
document.body.appendChild(div);
document.body.removeChild(div); // detached from DOM
cached = div; // reference keeps it alive
}
build();
Fix
function build() {
const div = document.createElement('div');
div.textContent = 'temp';
document.body.appendChild(div);
document.body.removeChild(div);
// no lingering reference
}
// or explicitly drop
cached = null;
Example 3: Unbounded Cache → LRU
Buggy
const cache = new Map();
function getUser(id) {
if (!cache.has(id)) cache.set(id, fetchUser(id));
return cache.get(id);
}
// Map grows forever
Fix (LRU)
class LRU {
constructor(limit = 100) { this.limit = limit; this.map = new Map(); }
get(k) {
if (!this.map.has(k)) return undefined;
const v = this.map.get(k); this.map.delete(k); this.map.set(k, v); return v;
}
set(k, v) {
if (this.map.has(k)) this.map.delete(k);
this.map.set(k, v);
if (this.map.size > this.limit) {
const oldest = this.map.keys().next().value;
this.map.delete(oldest);
}
}
}
const cache = new LRU(200);
Example 4: Interval Leak
Buggy
function start() {
const data = new Array(1e5).fill(0);
setInterval(() => console.log(data.length), 1000); // never cleared
}
start();
Fix
function start() {
const data = new Array(1e5).fill(0);
const id = setInterval(() => console.log(data.length), 1000);
setTimeout(() => { clearInterval(id); }, 5000); // or keep `id` and clear on teardown
}
Example 5: Observer Cleanup
const obs = new MutationObserver(() => {});
obs.observe(document.body, { childList: true });
// ... later, on teardown
obs.disconnect();
Example 6: Using WeakMap for DOM Metadata
const meta = new WeakMap();
function tag(el, info) { meta.set(el, info); }
function info(el) { return meta.get(el); }
// When `el` is GC’d, its WeakMap entry disappears.
8) A: Browser — Create, Detect, and Fix a Leak
Goal: Reproduce a leak, capture snapshots, fix it, verify.
Setup page (save as lab-a.html):
<!doctype html>
<html>
<body>
<button id="leak">Leak 10k nodes</button>
<button id="fix">Fix</button>
<script>
let stash = [];
document.getElementById('leak').onclick = () => {
const frag = document.createDocumentFragment();
for (let i = 0; i < 10000; i++) {
const el = document.createElement('div');
el.textContent = 'node ' + i;
// Attach a listener that captures `el` and `i`
el.addEventListener('click', () => console.log(i));
frag.appendChild(el);
stash.push(el); // keeps DOM alive even after removal
}
document.body.appendChild(frag);
// Remove from DOM (detached but referenced)
document.body.textContent = '';
};
document.getElementById('fix').onclick = () => {
// Cleanup: drop references and let GC collect
stash.forEach(el => el.replaceWith()); // ensure no lingering DOM
stash = [];
// Force microtasks; GC is nondeterministic, but memory should drop soon
Promise.resolve().then(() => console.log('cleanup requested'));
};
</script>
</body>
</html>
Measure:
Open Chrome DevTools → Memory → Take snapshot (Baseline).
Click Leak 10k nodes → Take snapshot.
Click Fix → Take snapshot.
Compare Retainers for the detached nodes; confirm that stash holds references pre-fix, and is absent post-fix.
Discussion:
Show how listeners and array references keep detached DOM alive.
Explain nondeterministic GC timing.
B: Node.js — Observe Heap Growth & Fix
File: b-leak.js
// Run: node b-leak.js
// Optional inspection: node --inspect lab-b-leak.js
const big = Buffer.alloc(10 * 1024 * 1024); // 10MB external memory
const stash
No comments:
Post a Comment