Memory: Heap, Stack & Garbage Collection
JS apna kachra khud saaf karta hai — par kabhi kabhi bhool jaata hai.
JavaScript uses two distinct memory areas — the Call Stack and the Heap. They serve completely different purposes.
Call Stack: A LIFO (Last In, First Out) structure for function calls. Default size is ~1MB. Each function call creates a "frame" containing local variables, parameters, and a return address. When a function returns, its frame is popped off.
Primitives (number, string, boolean, null, undefined, symbol, bigint) are stored directly in the stack frame. They are small and have fixed size.
Heap: A large, dynamic memory pool for objects and closures. Objects are allocated on the heap — the variable only holds a reference (pointer) to the heap location.
// Primitives — stored on stack (by value)
let a = 10;
let b = a; // b gets a COPY
b = 20;
console.log(a); // 10 — unchanged (different stack slots)
// Objects — stored on heap (by reference)
let obj1 = { x: 1 };
let obj2 = obj1; // obj2 gets a COPY of the REFERENCE
obj2.x = 99;
console.log(obj1.x); // 99 — obj1 affected! Same heap object!
// Stack overflow example
function infinite() {
return infinite(); // No base case — stack fills up
}
// infinite(); // RangeError: Maximum call stack size exceeded
New objects are allocated in New Space (young generation) — a small area, typically ~1-8MB. This space is divided into two semi-spaces: from-space and to-space.
Allocation in New Space is incredibly fast — just move a pointer forward. Faster than a traditional malloc.
Minor GC (Scavenge): When from-space fills up, V8 copies all live objects from from-space to to-space, then swaps the roles. Dead objects are simply abandoned — no explicit freeing needed.
Objects that survive 2 minor GC cycles get promoted to Old Space. Most objects die young, so there's very little to copy each time.
// Objects created inside functions usually die young
function processRequest(data) {
const temp = { ...data, processed: true }; // young gen
const result = transform(temp); // young gen
return result; // temp dies here
}
// temp and intermediate objects are collected in minor GC
// They never reach old generation — very cheap to collect
// Objects that SURVIVE (keep references) move to old gen
const cache = {};
function cacheData(key, value) {
cache[key] = { value, timestamp: Date.now() }; // survives!
}
// cache entries survive minor GC → promoted to old generation
Most objects die young. Short-lived temporaries (loop variables, intermediate results, function-scoped objects) are collected in fast minor GCs. Only long-lived objects (caches, closures, module-level state) reach old generation.
Old Space holds objects that survived the young generation. It's much larger and when it fills up, a Major GC (Mark-Compact) runs.
Phase 1 — Mark: Starting from "roots" (global variables, stack variables, closures), V8 walks every object reference and marks reachable objects as "live".
Phase 2 — Compact: Live objects are moved together to eliminate fragmentation. All references to those objects are updated. The freed gaps become available for new allocations.
Major GC is expensive: it can pause JS execution for 10-100ms (Stop-The-World). V8 has optimizations to reduce this.
// What counts as a "root" for GC marking:
// - Global variables (window.something, global.something)
// - Variables on the call stack (local variables in active functions)
// - Variables captured by closures still in memory
// Long-lived references prevent GC
const subscribers = new Map();
function subscribe(id, handler) {
subscribers.set(id, handler); // handler can't be GC'd!
}
// If you never call unsubscribe, all handlers stay in old gen forever
function unsubscribe(id) {
subscribers.delete(id); // Now GC can collect it
}
A memory leak happens when an object stays in memory longer than needed because something still holds a reference to it. GC can't collect it even though it's "logically dead".
Leak 1 — Forgotten event listeners: Add a listener, never remove it, element gets removed from DOM. The closure in the listener keeps everything alive.
Leak 2 — Accidental globals: Forget let/const → variable becomes window.leak → never collected.
Leak 3 — Closures holding large data: Inner function keeps the entire outer scope alive, including large objects you forgot about.
Leak 4 — Detached DOM nodes: DOM node removed from tree but a JS variable still references it.
Leak 5 — Timers not cleared: setInterval callback holds references to objects in its scope.
// Leak 1 — Forgotten event listener
function setupButton() {
const largeData = new Array(10000).fill('data');
const btn = document.querySelector('#btn');
btn.addEventListener('click', () => {
console.log(largeData.length); // closure holds largeData!
});
// Even if setupButton returns, largeData stays in memory
// because the listener still references it
}
// Fix: removeEventListener when done, or use { once: true }
// Leak 2 — Accidental global
function processData(items) {
result = items.map(x => x * 2); // forgot 'const'!
// result is now window.result — never GC'd
}
// Leak 3 — Detached DOM node
let button = document.querySelector('#btn');
button.remove(); // Removed from DOM
// But JS still holds 'button' variable → can't be GC'd
button = null; // Fix: clear the reference
WeakMap holds keys weakly — if no other reference to the key object exists, GC can collect both the key AND the WeakMap entry automatically. No manual cleanup needed.
WeakSet works the same way — holds objects weakly, no iteration possible.
WeakRef gives you an explicit weak reference. Call .deref() to get the object, or undefined if GC already collected it.
Use WeakMap for: per-object metadata without preventing GC, private class data patterns, caches keyed by objects.
// WeakMap — private data without memory leak
const privateData = new WeakMap();
class User {
constructor(name, secret) {
privateData.set(this, { secret }); // weakly referenced
this.name = name;
}
getSecret() {
return privateData.get(this).secret;
}
}
let user = new User('Sai', 'mypassword');
console.log(user.getSecret()); // 'mypassword'
user = null; // user object can now be GC'd
// When GC collects user, WeakMap entry automatically removed!
// No manual cleanup needed
// vs regular Map — MEMORY LEAK!
const regularMap = new Map();
regularMap.set(user, { secret: '123' }); // strong reference
// Even if user = null, Map keeps the entry forever
Since GC can remove entries at any time, iteration would give non-deterministic results. By design, you can only do keyed lookups by object reference. Use them for metadata, not for collections you need to iterate.
Lo kar liya — Key Points:
- ✅ Call stack stores function frames (local vars, params) — primitives here. Heap stores objects — variables hold references
- ✅ Stack overflow = too many nested calls. Heap overflow = too many objects not being GC'd
- ✅ Minor GC (young generation) is fast — most short-lived objects collected here cheaply
- ✅ Major GC (old generation) is expensive — mark-compact with potential JS pauses
- ✅ Memory leaks happen when references prevent GC: forgotten listeners, accidental globals, closures holding large data
- ✅ WeakMap/WeakSet hold keys weakly — GC can collect entries automatically when the key object dies
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login