Chapter 2.2☕ 22 min read

Memory: Heap, Stack & Garbage Collection

JS apna kachra khud saaf karta hai — par kabhi kabhi bhool jaata hai.

01Call Stack vs Heap

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
V8's heap is NOT one big block — it's divided into spaces: New Space (young generation), Old Space (old generation), Code Space (compiled code), Large Object Space (>256KB objects). Each space has different GC strategies.
02Young Generation: Minor GC (Scavenge)

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
📋 The generational hypothesis:
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.
03Old Generation: Major GC (Mark-Compact)

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
}
V8's Orinoco GC project made major GC mostly concurrent — marker threads run alongside JS execution. JS only pauses for short "stop-the-world" checkpoints, typically <1ms. Full pauses of 10-100ms are much rarer now than in older V8 versions.
04Memory Leaks: Common Patterns

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
Chrome DevTools Memory tab is your best friend for finding leaks. Take a heap snapshot before and after an action. If memory grows, compare snapshots — look for objects in "Detached DOM tree" or growing Maps/arrays that should have been cleared.
05WeakRef, WeakMap & Memory-Friendly Patterns

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
📋 Why WeakMap is not iterable:
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
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase