Memory & Garbage Collection
Advanced · 18 min read · ▶ live playground · ✦ checkpoint
Your code can stop mentioning an object, and the object can still stay alive. JavaScript memory is not about "did I set it to null?" It is about reachability: whether the engine can still follow a path to that value.
Stack frames and the heap
From 10.1, every function call creates a stack frame, one entry on the call stack that holds that call's temporary local names. Objects, arrays, and functions live in the heap, the storage room where JavaScript keeps object bodies. Local names in stack frames can hold reference tickets to those heap objects.
function makePlayerCard(playerName, startingScore) {
const card = {
playerName,
score: startingScore,
badges: ["starter"],
};
return card;
}
const activeCard = makePlayerCard("Mina", 18);
console.log(activeCard.playerName);
console.log(activeCard.badges[0]);→ Mina, then starter.
While makePlayerCard runs, its stack frame has local names like playerName, startingScore, and card. The object body lives in the heap. When the function returns, its frame pops off, but the returned reference ticket is copied into activeCard, so the heap object stays useful.
If no reachable name, array slot, object property, closure, timer, or listener list points at an object anymore, the engine is allowed to clean it up.
Reachability decides life
A root is a starting place the engine treats as definitely alive, such as global names, current stack frames, and functions held by the runtime, the browser machinery around the engine. An object is reachable when JavaScript can start at a root and follow reference tickets to it.
let currentPlayer = {
playerName: "Rae",
score: 31,
};
const bench = [currentPlayer];
currentPlayer = null;
console.log(bench[0].playerName);
bench.pop();
console.log(bench.length);→ Rae, then 0.
Setting currentPlayer = null removes one ticket. It does not destroy the object, because bench[0] still holds another ticket. After bench.pop(), the array no longer points at that object. If nothing else points at it, the object is now unreachable and can be collected later.
That "later" matters. You don't control the exact moment. You write code that removes unnecessary references, then the engine decides when cleanup is worth running.
How garbage collection works
Garbage collection is automatic memory cleanup: the engine finds heap objects your program can no longer reach and reclaims their space. The beginner model you need is mark-and-sweep, a cleanup pass with two moves:
- Mark: start from roots and mark every object that can still be reached.
- Sweep: reclaim heap objects that were not marked.
function formatRoundReport(playerName, scores) {
const report = {
playerName,
scores,
total: scores.reduce((sum, score) => sum + score, 0),
};
return `${report.playerName}: ${report.total}`;
}
console.log(formatRoundReport("Ira", [8, 10, 7]));→ Ira: 25.
report and the scores array are reachable while formatRoundReport runs. After the function returns, the stack frame is gone. The returned string remains, but the temporary report object is not needed anymore unless some other reachable value kept a ticket to it.
Real engines add many optimizations. One common idea is generational collection, which means the engine treats newer objects differently from older survivors. Most objects are short-lived: little arrays, temporary option objects, callback data. So the engine can check young objects often, and move long-lived survivors into an older group that is checked less often. You do not tune this in normal JavaScript; you help by not keeping old tickets accidentally.
Forgotten timers keep things reachable
A memory leak is memory your program no longer needs but still keeps reachable. Leaks in JavaScript usually come from one mistake: something outside your current function still has a reference ticket.
A timer can do that. A callback is a function handed to another part of the program so it can run later. The runtime holds the callback, and if that callback closes over an object, the object stays reachable too.
function startPracticeTimer(playerName) {
const session = {
playerName,
notes: ["warmup", "sprint"],
};
let ticks = 0;
const timerId = setInterval(() => {
ticks = ticks + 1;
console.log(`${session.playerName}: tick ${ticks}`);
if (ticks === 2) {
clearInterval(timerId);
console.log("timer cleared");
}
}, 5);
}
startPracticeTimer("Bo");→ Bo: tick 1, Bo: tick 2, then timer cleared.
If that interval never cleared, the runtime would keep the callback. The callback would keep its birthplace variables, including session. That is a classic forgotten-timer leak: the user moved on, but the timer still has a ticket.
Dangling listeners keep things reachable
A listener is a callback stored in a list so it can run when something happens. A dangling listener is a listener you forgot to remove after the owner is done.
This pure JavaScript scoreboard acts like a tiny listener list:
function createScoreBoard() {
const listeners = [];
return {
onScore(listener) {
listeners.push(listener);
return function stopListening() {
const index = listeners.indexOf(listener);
if (index !== -1) {
listeners.splice(index, 1);
}
};
},
score(points) {
for (const listener of listeners) {
listener(points);
}
},
listenerCount() {
return listeners.length;
},
};
}
const board = createScoreBoard();
const stopMina = board.onScore(points => console.log(`Mina saw ${points}`));
board.onScore(points => console.log(`Rae saw ${points}`));
board.score(5);
console.log(`listeners: ${board.listenerCount()}`);
stopMina();
board.score(8);
console.log(`listeners: ${board.listenerCount()}`);→ Mina saw 5, Rae saw 5, listeners: 2, Rae saw 8, then listeners: 1.
listeners is private state held by a closure, a function plus the birthplace it remembers. As long as board is reachable, that listener array is reachable. Each listener function inside it is reachable too, along with whatever each listener remembers from its birthplace.
The fix is boring and powerful: when a thing registers a listener, keep the cleanup function and call it when that thing is done.
Try it — clean up both handles
Run this, then comment out stopNia() and predict what stays alive longer. Leave clearInterval(timerId) in place; the timer experiment must stay bounded.
Most memory work is not manual freeing. It is removing references your program no longer means to keep: clear timers, unsubscribe listeners, drop old saved arrays, and let temporary objects fall out of reach.
You have now seen JavaScript's execution model from stack frames to queues to this to memory. Next, prototypes and classes build on the same system: functions and objects in the heap, connected by references, called through the rules you already know.
Checkpoint
Answer all three to mark this lesson complete