06 · 14 steps
Visualize / 06

Stack vs heap memory.

Two regions of the same address space, two completely different policies. The stack is automatic, fast, and tied to the lifetime of a function call. The heap is manual (or GC\'d), bigger, and survives whatever function created it. Watch makePoint() use both — a local int on the stack, a 16-byte struct on the heap — and see what the program actually does to each.


step 1 / 14
scenario main() → makePoint() returns Point* → print → free x lives on the stack · Point lives on the heap · free() returns it
STACK
HEAP · ALLOC
HEAP · FREE/LEAK
POINTER
SOURCE 1 struct Point { int x; int y; }; 2 3 Point* makePoint() { 4 int x = 5; // stack 5 Point* p = malloc(sizeof(Point)); // heap 6 p->x = 10; 7 p->y = 20; 8 return p; // pointer survives 9 } 10 11 int main() { 12 Point* q = makePoint(); 13 print(q->x); // 10 14 free(q); 15 return 0; 16 }▶ PC = highlighted lineVIRTUAL ADDRESS SPACE0x7FFF·high (top of stack)STACKgrows ↓— stack empty —unused virtual addresses (where stack and heap grow toward each other)HEAPgrows ↑— heap empty —0x0010·low (text/data/heap base)WHAT JUST HAPPENED— idle —CURRENT STATESTACK FRAMES0HEAP BLOCKS · ALIVE0LEAKED · BYTES0RELATIVE COSTSstack• alloc: 1 instruction• free: 1 instruction• cache: hot · same lines• limit: ~8 MBheap• alloc: ~20–200 ns• free: ~20–100 ns• cache: cold · scattered• limit: gigabytesRULE OF THUMBsmall + short-lived + size-known→ stack.large · or · must outlive caller→ heap.

Program loaded. Stack region reserved (typically 8 MB on Linux). Heap region empty — it'll grow as malloc() asks for bytes. The whole picture below is one process's virtual address space: low addresses at the bottom, high addresses at the top.

Stack
A region of memory used for function call frames. Allocated automatically on call, freed automatically on return. Fast, small, LIFO.
Heap
A larger region used for dynamic allocations via malloc/new. You ask for bytes, get a pointer, and are responsible for free()-ing it (or having a garbage collector do it).
Virtual address space
The OS gives each process its own private map from addresses to memory. Two processes can both use address 0x1000 — they refer to different physical bytes.

Why have two regions at all

Stack allocation is wildly fast — adjust a register, done. Stack frees are equally cheap — adjust the register back. Memory you allocate this way is also cache-hot because the same few hundred bytes get reused constantly. The catch is that everything in the stack dies when its function returns. If you need a value to outlive the call that created it, the stack can't help.

The heap exists for that case: bytes that live as long as you want, returned only when you call free (or when the GC decides nobody references them). The price is real overhead — the allocator walks free lists, the bytes you get are wherever there's space, your cache hates this — and the responsibility to release them.

Why dangling pointers are so dangerous

When you free heap memory, the allocator doesn't usually zero it. Your old data is still there for a while, until the next malloc hands those bytes to someone else. A dangling pointer can therefore appear to work — you read 10 from q->x long after free, because nothing has overwritten it yet. Then someone else allocates, your old bytes become their struct, and now your "10" is their length field. The bug doesn't crash where it happens, it crashes minutes later in unrelated code.

The same goes for use-after-free on the stack. Return a pointer to a local int x, the caller dereferences it, and gets... whatever the next function call decided to put in that 4 bytes. Sometimes it works. Sometimes you get garbage. Sometimes you write to memory that another stack frame thinks it owns.

How modern languages handle this

  • C / C++ (manual). You free what you malloc. Smart pointers (unique_ptr, shared_ptr) automate the call to delete at scope exit (RAII).
  • Rust (ownership). The compiler proves at build time that every heap object has exactly one owner. When the owner goes out of scope, the object is freed. Bugs in this class become compile errors.
  • Go / Java / C# / JS (garbage collected). The runtime tracks what references each heap object. When no references remain, the GC reclaims it. Costs throughput and occasional pauses, buys no manual frees.
  • Python. Reference counting for the common case + a generational GC to catch cycles. Almost as no-thought as Java, less throughput.
  • Swift / Obj-C. Reference counting (ARC) inserted by the compiler. No GC pauses, but cycles need weak references or they leak.

When does data go on the heap automatically?

In a managed language you usually don't choose. JVM and V8 allocate almost everything on the heap because they can\'t prove the lifetime statically. Escape analysis is the optimisation that says "this object never leaves the function" — it gets stack-allocated instead, transparently. Go does this aggressively; you can see it with go build -gcflags="-m".

In C/C++ you choose explicitly: variables declared inside a function go on the stack (or in registers); malloc/new goes on the heap. Globals and statics live in their own region (the data segment), neither stack nor heap.

Go deeper

Memory in the Computer Architecture Codex →

Virtual memory, the page table, mmap, the difference between RSS and VSZ, why your process can "use" 4 GB on a 2 GB machine.

Open the Codex
Found this useful?