The Archive
Foundations

Memory, References & Ownership

Where values actually live, what a pointer points at, and who is allowed to free them.

Prerequisites

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, malloc hands you raw bytes, and free is 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.
Rendering diagram…

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:

Rendering diagram…

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.

Naive vs. safe — sharing a heap block
// 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}
switch:

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).

Rendering diagram…

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.
Rendering diagram…

The invariants

Each model has one rule that keeps it sound:

  • Manual (C): every malloc has exactly one free, 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:

StepabNote
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

Aliasing, copying, and moving — three languages, one value
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 untouched
switch:

Cost table

Memory is not free — the shape of your access pattern is what these costs measure.

OperationBestAverageWorstWhy
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.getrefcount free 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.
Ownership rule you'll meet everywhere: one owner, many borrowers
// 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}
switch:

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.

On this page