Memory, References & Ownership
Where values actually live, what a pointer points at, and who is allowed to free them.
Prerequisites
- Big-O & Asymptotic Analysis — allocation, copying, and cache costs all show up as growth.
- Recursion & the Call Stack — the stack we met there is one half of the memory story.
The 10-second version
Your program's memory is one enormous linear array of numbered bytes, and every value lives at some address in it. A variable can hold a value directly (value semantics) or hold an address that points somewhere else (reference semantics). The moment two names point at the same bytes you get aliasing — powerful, but it's the source of the nastiest bugs in the field: double frees, dangling pointers, data races. Three eras of answers grew up around who is allowed to free what: manual (C), garbage-collected (Java, JS, Python), and owned-and-borrowed (Rust). This page is the foundation under why an array and a linked list, holding the same data, behave so differently.
Why this existed
Early machines had wires and plugs; there was no abstraction between you and the hardware. In 1945, John von Neumann (building on work by Alan Turing and the ENIAC team) circulated the First Draft of a Report on the EDVAC, describing a machine where both instructions and data live together in one addressable memory. That is the stored-program computer — and it's why your data and your code compete for the same RAM.
Once everything is a numbered slot, you have to decide: do variables hold the thing, or the address of the thing? That single question split programming languages for the next 70 years.
- 1960 — Lisp introduces automatic memory management: a garbage collector reclaims
anything unreachable, so you stop thinking about
free. - 1972 — C does the opposite and makes the machine visible: a pointer is a number,
mallochands you raw bytes, andfreeis entirely your problem. - 1995 — Java sells tracing GC to the mainstream: allocate freely, and a background collector cleans up after you.
- 2006 — Graydon Hoare starts Rust at Mozilla, looking for a third answer: deterministic, no GC, and compile-time proof that you never dangle or race.
Why it mattered then: as programs grew, "someone must own this block" became the central question of reliability. A double free or a use-after-free is not a logic bug you can read your way out of — it's memory corruption that shows up miles away from the mistake.
Inside the inventor's head
Start with the picture von Neumann's report makes inevitable. There's a stack for the frames we met in the last page — it grows and shrinks in order, and it's blazing fast — and there's a heap, a big lazy pool you request from when the size or lifetime isn't known up front. A pointer is just a byte index into that heap.
The first attempt at managing it goes like this: "I'll allocate a block, use it, and free it when I'm done." Then watch the trap that shipped in a million C programs:
Two names, one block, and no rule about who may free it. Whoever wrote the code last has to guess. That's the whole reason GC and ownership were invented: to make "who owns this?" an answerable, checkable question instead of a convention.
// JS: you never free anything. The collector frees what's unreachable.function share() { const owner = [1, 2, 3, 4]; const alias = owner; // same array, two names, totally fine alias[0] = 42; return alias; // still reachable — GC keeps it alive}The pivot in all three is the same: stop letting anyone free, and instead make ownership a property the language can check. C checks nothing; Java/JS check reachability at runtime; Rust checks at compile time.
The final approach
The memory picture
Two regions, one address space. Values on the stack die when their scope ends; values on the heap live until freed (or collected, or dropped).
Value vs. reference semantics
- Value semantics: the variable is the bytes. Assigning copies them.
- Reference semantics: the variable holds an address. Assigning copies the address, so both names now reach the same object.
The invariants
Each model has one rule that keeps it sound:
- Manual (C): every
mallochas exactly onefree, on a pointer that is still valid. The compiler does not enforce this — that's the bet C makes. - GC (Java/JS/Python): any object reachable from a root (stack, globals, registers) is kept; everything else is garbage. Reachability, not ownership, decides life.
- Ownership (Rust): each value has exactly one owner; borrowing is temporary and the compiler proves no reference outlives its data. This is the borrow checker.
A worked trace: aliasing
Watch what "same bytes, two names" does when one name mutates:
| Step | a | b | Note |
|---|---|---|---|
| 1 | [1, 2, 3] | — | allocate array, a references it |
| 2 | [1, 2, 3] | [1, 2, 3] | b = a — b aliases the same array |
| 3 | [9, 2, 3] | [9, 2, 3] | write through b — a sees it too |
That's the reason a "copy my array" line often doesn't copy anything. In JS you wanted
[...a]; in C you wanted memcpy; in Rust the compiler would have stopped you from touching
a while b borrows it.
It in code
const a = [1, 2, 3];const alias = a; // reference: same arrayalias[0] = 9; // a[0] is now 9 too!const copy = [...a]; // value semantics: a genuine copycopy[0] = 100; // a is untouchedCost table
Memory is not free — the shape of your access pattern is what these costs measure.
| Operation | Best | Average | Worst | Why |
|---|---|---|---|---|
| Allocate on the stack | Θ(1) | Θ(1) | Θ(1) | bump one pointer |
| Allocate on the heap | Θ(1) | Θ(1) | Θ(n) | allocator may scan free lists |
| Dereference a pointer | Θ(1) | Θ(1) | Θ(1) | one indirection — but may miss cache |
| Deep copy a value | Θ(n) | Θ(n) | Θ(n) | n = size in fields / bytes |
| Move an owner (Rust) | Θ(1) | Θ(1) | Θ(1) | just a pointer copy |
| Drop / ref-count to zero | Θ(1) | Θ(1) | Θ(n) | freeing a chain can cascade |
The towering cost that's not in the table is the cache miss. A pointer chase to a random heap address can cost 100× a stack hit — which is exactly why contiguous structures (arrays) beat pointer-chasing ones (linked lists) even when the Big-O is identical.
Where you meet it today
- Garbage collectors. V8, the JVM, Go, and Python all trade predictable pauses for your convenience. You feel it as a stutter during a big allocation.
- Reference counting. Swift's ARC and Python's
sys.getrefcountfree an object the instant its last reference dies — no global scan, but cycles need special handling. - The borrow checker. Rust compiles rules into compile errors: no use-after-free, no data races, no double free — verified before the program ever runs.
- Cache locality. Game engines and databases reorder data — array-of-structs → struct-of-arrays — purely to make the bytes that get read together sit together.
// JS: the runtime decides. Reachable values survive; the rest is collected.function cache() { const box = { seen: new Map() }; // reachable from the closure below return () => box.seen; // keeps `box` alive as long as this lives}Recap
- Memory is a flat array of addressed bytes; values live on the stack or the heap, and a pointer is just an address into it.
- Value semantics copies bytes; reference semantics copies an address, which creates aliasing — the root of double-free, dangling, and race bugs.
- Three answers to "who frees it?": manual (C), reachability by GC (Java/JS), and compile-time ownership + borrows (Rust).
- The scary costs are hidden: a pointer chase can be 100× slower than touching the next byte in a contiguous block — the seed of the array-vs-linked-list argument.
Next up: Arrays — the contiguous structure where that address arithmetic pays off instantly.