Memory Management
JavaScript manages memory automatically through garbage collection. Learn how the heap and stack work, what causes memory leaks, and how to profile and fix them.
Memory in JavaScript
JavaScript automatically allocates and frees memory โ you don't use malloc/free like in C. But understanding memory helps you avoid memory leaks and write efficient code.
Two main memory locations:
| Stack | Heap | |
|---|---|---|
| Stores | Primitives, references, call frames | Objects, arrays, functions |
| Size | Fixed, small | Dynamic, large |
| Access | Very fast (LIFO) | Slower (pointer dereference) |
| Lifetime | Automatic (frame exit) | Managed by GC |
The Stack
The stack holds:
- Primitive values (
number,boolean,string,null,undefined,symbol,bigint) - References (pointers) to heap objects
- Execution context frames
function example() {
const a = 42; // number โ stored on stack
const b = "hello"; // string โ stack (small strings may be interned)
const c = { x: 1 }; // reference on stack, object on heap
// Stack frame for example() is added
// When example() returns, frame (and a, b, c) are removed
}
The Heap
The heap stores all objects, arrays, and functions. Memory here persists until the garbage collector reclaims it.
const obj = { name: "Alice" }; // { name: "Alice" } lives on the heap
// obj (the reference) is on the stack
Garbage Collection
V8 uses a generational garbage collector:
Young Generation (Nursery)
Most objects are short-lived. New objects start here:
- Small space (~1-8MB)
- Collected frequently using Scavenge (copying GC)
- Survivors are promoted to old generation after 2 collections
// These temporary objects live briefly in young gen:
function processData(items) {
return items.map(x => ({ value: x * 2 })); // temporary objects
}
// After processData returns, these objects become unreachable โ GC reclaims
Old Generation
Long-lived objects (survived young gen twice) move here:
- Larger space
- Collected with Mark-and-Compact (full GC) โ more expensive but less frequent
- V8 runs these incrementally to avoid long pauses
Mark-and-Sweep
The fundamental GC algorithm:
- Mark phase: Starting from "roots" (global variables, stack variables), trace all reachable objects and mark them
- Sweep phase: Reclaim memory of unmarked (unreachable) objects
let obj1 = { name: "Alice" }; // reachable via obj1
let obj2 = { name: "Bob" }; // reachable via obj2
obj1 = null; // { name: "Alice" } is now unreachable โ eligible for GC
Memory Leaks
A memory leak is memory that's allocated but never freed โ even though you're done with it. Common causes:
1. Forgotten Event Listeners
// โ Leak: adding listeners without removing them
function setupUI() {
const handler = () => console.log("click");
document.getElementById("btn").addEventListener("click", handler);
// if this function is called many times, listeners pile up!
}
// โ Remove when done
function setupUI() {
const handler = () => console.log("click");
const btn = document.getElementById("btn");
btn.addEventListener("click", handler);
// Return cleanup function
return () => btn.removeEventListener("click", handler);
}
2. Closures Holding Large References
// โ The closure captures a large array unnecessarily
function processLargeArray() {
const largeData = new Array(1_000_000).fill({ x: Math.random() });
return function() {
// Only needs one value, but the entire array stays in memory
// because the closure references `largeData`
return largeData[0];
};
}
// โ Extract only what you need before closing
function processLargeArray() {
const largeData = new Array(1_000_000).fill({ x: Math.random() });
const firstItem = largeData[0]; // extract
// largeData can now be GC'd after this function returns
return function() {
return firstItem; // closure only captures firstItem
};
}
3. Global Variables
// โ Accidentally global (missing var/let/const)
function createUser() {
userData = { name: "Alice" }; // no declaration! becomes global
}
// โ Always declare
function createUser() {
const userData = { name: "Alice" };
}
4. Detached DOM Nodes
// โ Reference to removed DOM element
let detachedNode;
function createLeak() {
const node = document.createElement("div");
document.body.appendChild(node);
detachedNode = node; // hold reference
document.body.removeChild(node); // remove from DOM
// node is removed from DOM but detachedNode still references it!
}
// โ Clean up references
function noLeak() {
const node = document.createElement("div");
document.body.appendChild(node);
document.body.removeChild(node);
// No reference held โ GC can reclaim
}
5. Caches Without Limits
// โ Unbounded cache grows forever
const cache = {};
function memoize(fn) {
return function(key) {
if (!cache[key]) {
cache[key] = fn(key); // memory grows indefinitely
}
return cache[key];
};
}
// โ Use WeakMap (or LRU cache with size limit)
const weakCache = new WeakMap();
function memoizeObject(fn) {
return function(obj) { // obj must be a reference type
if (!weakCache.has(obj)) {
weakCache.set(obj, fn(obj));
}
return weakCache.get(obj);
// When obj is no longer referenced, WeakMap entry is GC'd automatically
};
}
WeakMap and WeakSet
WeakMap and WeakSet hold weak references โ they don't prevent GC:
const wm = new WeakMap();
let user = { name: "Alice" };
wm.set(user, { sessions: 5 }); // user โ data
user = null; // user object becomes unreachable
// WeakMap entry is automatically cleaned up โ no memory leak!
// Regular Map would keep the entry alive:
const m = new Map();
let obj = {};
m.set(obj, "data");
obj = null; // Map still holds a reference โ obj is NOT GC'd!
Profiling Memory in DevTools
In Chrome DevTools โ Memory tab:
- Heap Snapshot โ see what's currently in memory
- Allocation instrumentation โ track allocations over time
- Allocation sampling โ lightweight profiling
// To find leaks:
// 1. Take a heap snapshot (baseline)
// 2. Perform the action suspected of leaking (e.g., open/close a modal)
// 3. Take another heap snapshot
// 4. Compare snapshots โ objects that grew indicate a leak
Key Takeaways
- Stack: primitives + references, fast, auto-freed when function returns
- Heap: objects + arrays + functions, managed by GC
- V8's GC is generational: young gen (frequent, fast) + old gen (infrequent, thorough)
- Mark-and-sweep: trace reachability from roots, reclaim unreachable objects
- Common memory leaks: forgotten event listeners, closures over large data, globals, detached DOM nodes, unbounded caches
- Use
WeakMap/WeakSetfor caches โ entries are GC'd when keys are unreachable
Ready to test your knowledge?
Take a quiz on what you just learned.