Intuition
Think of a desk and a warehouse. Each time you start a task you put a fresh sheet of paper on top of a pile on your desk and scribble the task’s notes on it. When the task is done, you throw away the top sheet. That pile is the stack: fast, tidy, and everything on a sheet disappears with it.
Some things are too big for a sheet, or need to stay around after the task ends: a list of a million names, an object you’ll hand to someone else. Those go in the warehouse, and your sheet just notes the shelf number. That’s the heap, and the shelf number is a reference (a pointer).
How it works
When a function is called, the program pushes a stack frame holding the call’s parameters and local variables. When the function returns, the frame is popped and all of it is gone at once. Nothing has to be cleaned up one variable at a time, which is why the stack is so cheap.
When code creates something that must outlive the call, or whose size isn’t known in advance, it is allocated on the heap. The variable on the stack holds a reference to it. Several references can point at the same object, and the object stays alive while at least one does.
In the figure:
demo(3)runs withcountin its frame.- Calling
build(count)pushes a new frame and copies the number inton. [0] * ncreates a list on the heap;numsonly holds a reference to it.buildreturns. Its frame is popped, but the list survives becausesquaresnow refers to it.alias = squarescopies the reference, so a change throughaliasshows up insquarestoo.
Stack vs heap at a glance
| Aspect | Stack | Heap |
|---|---|---|
| Holds | Parameters, locals, return addresses | Objects that outlive a call or vary in size |
| Lifetime | Until the function returns | Until nothing refers to it (or it is freed) |
| Allocating | Move the stack pointer: almost free | Ask the allocator to find a free block |
| Freeing | Automatic on return | Garbage collector, owner, or free() |
| Size | Small and fixed: about 1–8 MB per thread | Up to the machine’s memory |
| Goes wrong as | Stack overflow from deep recursion | Leaks, fragmentation, use after free |
In each language
| Language | What goes where |
|---|---|
| Python | Every value is an object on the heap; frames hold references. The garbage collector (reference counting plus a cycle finder) frees objects. |
| JavaScript | Objects and arrays live on the heap; engines keep small numbers inline. A garbage collector frees what is unreachable. |
| Go | The compiler decides by escape analysis: a slice returned from a function escapes, so it goes on the heap. A slice variable is a small header (pointer, length, capacity) that points at its array. |
| Rust | Vec, Box and String put their data on the heap. Each value has one owner, and the memory is freed when the owner goes out of scope; there is no garbage collector. |
| C | malloc gives you heap memory and free gives it back. Forgetting free leaks; using memory after free is undefined behaviour. |
| Java | Objects are on the heap; local primitives (int, double) and references live in the frame. |
Common mistakes
- Thinking assignment copies a list.
b = acopies the reference; usea.copy()(Python),[...a](JavaScript) orslices.Clone(Go) for a separate list. - Returning a pointer to a local in C. The local’s frame is gone after the return, so the pointer dangles.
- Huge arrays as locals. A 10 MB local array overflows a 1 MB stack. Put it on the heap.
- Blaming the heap for slowness. Allocation is slower than the stack, but rarely the bottleneck. Measure before rewriting.
Check yourself
Check yourself
After build returns, why is the list still there?
build’s frame (with n and nums) is gone, but the list was never in it. It lives on the heap and stays alive while something, here squares, refers to it.
Check yourself
b = a where a is a list, then b[0] = 5. What is a[0]?
Assignment copied the reference, not the list, so a and b are two names for one list. Changing it through b changes what a sees.
Sources
- Randal E. Bryant and David R. O’Hallaron, Computer Systems: A Programmer’s Perspective, 3rd ed., chapters 3.7 (procedures and the run-time stack) and 9.9 (dynamic memory allocation).
- The Go FAQ, “How do I know whether a variable is allocated on the heap or the stack?”
- The Rust Programming Language, chapter 4, “Understanding Ownership”.
Finished this page?
Your list