Memory Models
Date: 2026-08-17
Where values actually live, and what a variable holds. Almost every “why did that change?” and “why didn’t that change?” bug in application code is one question underneath: is this a value or a reference?
A memory model describes how a runtime stores data and what a variable name refers to. Two regions matter:
STACK HEAP
fixed size, fast large, flexible
one frame per function objects live here
call, popped on return freed by GC
holds: primitives, holds: objects,
references arrays, functions,
closures
Value versus reference
The distinction that explains most of it:
// primitives — copied by VALUE
let a = 5
let b = a // b gets its own 5
b = 10 // a is still 5
// objects — copied by REFERENCE
let x = { n: 5 }
let y = x // y points at the SAME object
y.n = 10 // x.n is now 10 stack heap
a → 5
b → 5
x → 0x1f4 ────┐
y → 0x1f4 ────┴──→ { n: 10 }
Two names, one object. That’s the whole mechanism behind accidental mutation — Immutability.
”Pass by reference” is usually wrong
JavaScript, Java, Python and most modern languages are pass by value — the reference is what gets copied.
function reassign(o) { o = { n: 99 } }
function mutate(o) { o.n = 99 }
const obj = { n: 1 }
reassign(obj) // obj.n is still 1
// the local copy of the
// reference was repointed
mutate(obj) // obj.n is now 99
// followed the referenceMutating what a reference points at works. Reassigning the reference doesn’t. The term for this is pass by sharing, and knowing it predicts which of those two lines does what.
Equality follows from it
{ a: 1 } === { a: 1 } // false
[1,2] === [1,2] // false
const o = { a: 1 }
o === o // trueObject equality compares addresses, not contents. Which is inconvenient for comparison and extremely convenient for change detection — an O(1) identity check is only a valid “did this change” test because references work this way. It’s why immutable updates and UI framework re-rendering are the same topic.
The stack is small and fixed
RangeError: Maximum call stack size exceeded
Each call pushes a frame holding parameters, locals and the return address. The stack has a fixed budget — on the order of ten thousand frames, varying by engine and by how much each frame holds — so deep recursion exhausts it long before memory runs out — Recursion.
The heap is large and dynamic and runs out differently: gradually, as unreachable objects accumulate faster than they’re collected — Garbage Collection.
What keeps objects alive
An object survives while it is reachable from a root — a global, a live stack frame, or a closure.
function makeHandler(bigData) {
return () => bigData.id // closes over
} // bigData
// the whole bigData object stays alive
// as long as the handler does — even
// though only .id is usedClosures capture the variable, not the value. A handler attached to an element, holding a large object, keeps that object alive until the handler is removed — which is the shape of most front-end memory leaks: detached DOM nodes, uncleared intervals, and listeners never unbound.
Practical consequences
- Copy before mutating anything you didn’t create.
{...obj},[...arr]— shallow, so nested objects are still shared constprevents reassignment, not mutation.const arr = []still allowspush- Default parameters and shared objects. A default object argument is created per call in JS; in Python it is created once at definition, which is the classic mutable-default bug
- Don’t compare objects with
===expecting value semantics. Compare a stable key, or serialise deliberately - Remove listeners and clear timers on teardown. This is the whole of front-end leak prevention in one line
Where it stops being your problem
Managed runtimes hide allocation and freeing, so you never call malloc or free. What they don’t hide is reachability — you decide what stays alive by deciding what still holds a reference. That’s the part that remains yours in every garbage-collected language.