- Introduction — Where Do Variables Live?
- A Process's Memory Map
- The Stack — Frames, LIFO, and Speed
- The Heap — Dynamic and Free
- Pointers and References — The Bridge Between Two Worlds
- Value Semantics vs Reference Semantics
- Stack Overflow vs OOM — Two Kinds of Death
- Why Recursion Depth Matters
- Cleaning Up the Heap — Ownership, GC, and Manual Management
- Pitfalls You Hit in Practice
- Conclusion
- References
Introduction — Where Do Variables Live?
At some point while learning to program, you hear that "this thing lives on the stack and that one lives on the heap." You nod and move on, but the idea keeps tripping you up. Why do some values vanish when a function returns while others survive? Why does deep recursion crash your program? Why must you free what you malloc in C, but not in JavaScript? Why does copying a list in Python sometimes mutate the original?
At the root of all these questions sit the stack and the heap. Regardless of language, these are the two fundamental ways a program manages memory. The names come from data structures, but here the story is about runtime: where a value is stored and when it disappears. This post digs into that truth from the ground up.
A Process's Memory Map
When a program runs, the operating system hands the process a virtual address space. That space is carved into several regions.
High addresses
+---------------------------+
| Stack | <- grows downward
| | |
| v |
| |
| ^ |
| | |
| Heap | <- grows upward
+---------------------------+
| BSS (uninitialized) |
+---------------------------+
| Data (initialized) |
+---------------------------+
| Text (code) |
+---------------------------+
Low addresses
The interesting part is that the stack and the heap grow toward each other from opposite ends of the same address space. The stack grows from high addresses toward low ones; the heap grows from low toward high. Laid out this way, each grows in its own direction and they only collide in the extreme case where memory is truly exhausted. The two stars of this post are these regions: the stack and the heap.
The Stack — Frames, LIFO, and Speed
The stack is the memory that manages function calls. Every time you call a function, a stack frame is pushed onto the stack. That frame holds the function's local variables, its parameters, and the return address to jump back to.
The key property is that this pushing and popping is strictly LIFO (Last In, First Out). The most recently called function finishes first, and its frame is popped first. It works exactly like stacking plates and taking them off from the top.
Call flow: main() -> a() -> b()
Stack (grows upward here):
+-----------+
| b() | <- currently running (top)
+-----------+
| a() |
+-----------+
| main() |
+-----------+
When b() returns, only b's frame is popped; control returns to a()
This structure is what makes the stack blazingly fast. Allocating and freeing memory is essentially just moving a single pointer. The CPU has a stack pointer register that points at the top of the stack. To push a frame, you decrement that pointer (the stack grows downward); to pop, you increment it back. No complex management, no searching for free space. Just one arithmetic operation.
void b() {
int y = 20; // stored in b's frame
}
void a() {
int x = 10; // stored in a's frame
b(); // b's frame is pushed on top
// when b() returns, y vanishes instantly
}
Another trait of the stack is that its size is fixed and usually small. The OS pre-assigns each thread a fixed stack size (commonly 1–8 MB). That limit is the source of the stack overflow we discuss later. In short: the stack is fast, automatically managed, has a lifetime tied exactly to the function that owns each frame, and is limited in size.
The Heap — Dynamic and Free
If the stack is bound to the lifetime of a function, the heap is the space freed from that constraint. A value on the heap can outlive the function that created it, and the program can allocate whatever size it decides at runtime. But this freedom comes at a price.
To get memory from the heap, you have to ask for it explicitly. In C, malloc plays that role.
#include <stdlib.h>
int *make_array(int n) {
// ask the heap for space for n integers
int *arr = malloc(n * sizeof(int));
for (int i = 0; i < n; i++) {
arr[i] = i * i;
}
return arr; // this memory survives after the function returns
}
Here the decisive difference from the stack appears. When make_array returns, the local variable arr (the pointer itself) disappears from the stack. But the array on the heap that arr pointed to stays alive. So the caller can take that address and keep using it.
This is also why heap allocation is slower than stack allocation. The allocator has to find "where is there free space that fits the requested size?" As memory is allocated and returned over and over, the heap fragments like cheese full of holes, and the allocator must search structures like a free list or size-class bins to pick a suitable spot. That searching and bookkeeping is incomparably more expensive than the stack's simple pointer bump.
Stack allocation: move the stack pointer (1 op) -> very fast
Heap allocation: search for free space + update metadata -> relatively slow
In summary, the heap is flexible (free size and lifetime) and lets you share values across function boundaries, but allocation is slow, fragmentation happens, and it must eventually be cleaned up. Who does that cleanup is a great fork in the road between languages, which we cover later.
Pointers and References — The Bridge Between Two Worlds
How are the stack and the heap connected? The answer is pointers or references. A value on the heap has no name. malloc simply hands back an address. The variable that holds that address, usually one sitting on the stack, is the pointer.
Stack Heap
+-----------+ +---------------------+
| ptr | --------> | actual data [42] |
| (address) | | (allocated by malloc)|
+-----------+ +---------------------+
this variable this data
lives on the stack lives on the heap
So the typical arrangement is this: a small value (usually an 8-byte address) sits inside a stack frame, and the large data it points to sits on the heap. It's like holding a big box on the heap by a small handle on the stack.
This idea is everywhere; only the syntax differs by language. C's pointers, C++'s references and smart pointers, Java's object references, every variable in Python, JavaScript's objects. They all share the same skeleton: the real data lives somewhere on the heap, and the variable points at its location. High-level languages merely hide this fact; they do not remove it.
Once you understand pointers, several long-puzzling phenomena click into place at once. If two variables point to the same heap object, changing the object through one makes the change visible through the other. The data was not copied; only the address was. That point is the heart of the next topic: value versus reference semantics.
Value Semantics vs Reference Semantics
When you assign a variable to another or pass it to a function, what gets copied? The answer to that question divides value semantics from reference semantics.
Under value semantics, the value itself is copied. The copy is completely independent of the original, so changing one leaves the other untouched. Primitive types like integers and floats usually follow value semantics, and they typically sit directly on the stack.
Under reference semantics, it is the reference (the address) that gets copied, not the value. So the copy and the original point to the same heap object, and a change through one is visible through the other.
# Python: integers behave like values, lists behave like references
a = 5
b = a
b += 1
print(a, b) # 5 6 — a is unchanged; they are independent
x = [1, 2, 3]
y = x # copies the reference (points to the same list)
y.append(4)
print(x) # [1, 2, 3, 4] — x changed too!
Because the rules differ by language, this difference is a common source of bugs. Java passes primitives (int, double, etc.) by value and objects by reference. JavaScript treats numbers and booleans as values, objects and arrays as references. C++ copies by value by default but lets you explicitly choose reference semantics with references (&) and pointers (*).
Miss this in practice and you hit the baffling situation where "I clearly copied it, but the original changed." The fix is to make intent explicit. If you truly need an independent copy, do a deep copy on purpose; if sharing is the intent, leave the reference alone. Knowing which is which is what matters.
Stack Overflow vs OOM — Two Kinds of Death
The stack and the heap run out differently, and as a result programs die differently too.
A stack overflow happens when you exhaust stack space. Since the stack has a fixed size (usually a few MB), pushing too many frames breaks the limit. The most common cause is runaway recursion.
def recurse(n):
return recurse(n + 1) # no base case
recurse(0)
# RecursionError: maximum recursion depth exceeded (Python)
# in other languages this can crash with a segmentation fault instead
Every call pushes a frame, and none get popped, so the stack hits its limit. In low-level languages like C, this is precisely the infamous "stack overflow" and a frequent cause of segmentation faults. Python adds a safety net: it counts recursion depth itself and raises RecursionError before the real stack overflows.
OOM (Out Of Memory) happens when the heap runs out. If you keep allocating on the heap without returning memory (a leak), or if you genuinely request more data than the system can handle, the heap runs dry.
Stack overflow:
cause - too-deep calls/recursion, too many frames
limit - thread stack size (usually 1-8 MB)
symptom - immediate crash, RecursionError, segfault
OOM (out of memory):
cause - accumulated heap allocations, leaks, huge data
limit - available physical/virtual memory (many GB+)
symptom - allocation failure, OOM killer, slow death via swapping
The two deaths have different characters. A stack overflow usually blows up instantly and deterministically, and it is easy to reproduce, so the cause (typically recursion) is clear. OOM often creeps up on you. A leak accumulates over hours until, at some moment, the system crawls under swapping or the OS's OOM killer force-kills the process. That makes OOM trickier to diagnose and often requires tools like profilers and heap dumps.
Why Recursion Depth Matters
Recursion is elegant, but it collides head-on with the stack's limits. Each recursive call pushes one stack frame, so the depth of the recursion is your stack usage. When depth exceeds the stack limit, the program dies.
# this recursion goes as deep as the list is long
def sum_list(items):
if not items:
return 0
return items[0] + sum_list(items[1:]) # depth = len(items)
sum_list(list(range(100000))) # blows the stack
If the list has 100,000 elements, the recursion depth becomes 100,000, which sails well past the default stack limit in most languages. There are two directions for a fix.
First, convert to iteration. A loop does not push frames; it runs within a single frame, so it consumes no stack. Rewriting the function above as a simple for loop makes it safe for any length of list.
Second, tail call optimization (TCO). If the recursive call is the very last thing the function does (no further operation is applied to its return value), the compiler can reuse the current frame instead of pushing a new one. Then recursion effectively behaves like iteration and the stack does not grow.
Ordinary recursion: each call pushes a frame -> stack grows with depth
Tail recursion: last call reuses the frame -> constant stack (with TCO)
Iteration: pushes no frames -> stack always constant
Be careful: not every language supports TCO. Scheme and some functional languages guarantee it by standard, but Python deliberately does not (to keep stack traces readable), and Java and many mainstream languages do not do it by default either. So relying on deep tail recursion without checking "does this language do TCO?" is dangerous. The safe default posture is: "any recursion that could get deep should be written as iteration or with an explicit stack data structure."
Cleaning Up the Heap — Ownership, GC, and Manual Management
Memory allocated on the heap must eventually be returned. If it is not, leaks pile up and lead to OOM. Languages solve this "when and by whom is it returned?" problem in three ways. This choice profoundly shapes the character of each language.
1. Manual management (C family). The programmer allocates and frees directly. What you get with malloc you must return with free.
char *buf = malloc(1024);
// ... use buf ...
free(buf); // forget it and you leak, do it twice and you crash
This approach gives maximum control and performance. The programmer decides exactly when memory is returned. But the room for error is large. Forget to free and you leak; use something after freeing it and you get a use-after-free; free it twice and you get a double-free. These bugs are also perennial sources of security vulnerabilities.
2. Garbage collection (GC — Java, Python, JavaScript, Go). The runtime automatically finds heap objects that "nobody references anymore" and reclaims them. The programmer does not worry about freeing.
let obj = { data: [1, 2, 3] };
obj = null; // now nothing points at that object
// the GC will reclaim it eventually. there is no free()
GC greatly improves memory safety. Whole classes of bugs like use-after-free and double-free simply disappear. But there is a price. While the GC runs, the program can briefly pause (a stop-the-world pause) or slow down, and the programmer cannot control exactly when reclamation happens. The GC itself also spends CPU and memory. In systems where real-time behavior matters, this unpredictability can be a problem.
3. Ownership (Rust). Rust takes a third path. There is no GC and no manual free. Instead, the compiler tracks each value's lifetime at compile time via ownership rules, and inserts code to return the memory automatically the moment the owner goes out of scope.
{
let s = String::from("hello"); // s owns the heap string
// ... use s ...
} // s goes out of scope here -> heap memory freed automatically (drop)
The core rule is: "a value has exactly one owner, and when the owner goes away, the value is freed." Add borrowing rules on top, and the compiler catches use-after-free and data races at compile time. As a result, Rust achieves memory safety without a runtime GC. The price is the learning curve; you spend time wrestling with the compiler to satisfy the ownership and borrowing rules.
Here is a side-by-side comparison of the three.
| Aspect | Manual (C) | GC (Java/Python/JS) | Ownership (Rust) |
|---|---|---|---|
| When freed | programmer specifies | runtime, later | automatically at scope end |
| Performance | best, predictable | GC overhead / pauses | near-best, predictable |
| Safety | low (leaks, UAF) | high | high (compile-time guarantee) |
| Burden | manual-management mistakes | hard to control timing | learning curve, fighting the compiler |
| Representative | C, some C++ | Java, Python, JS, Go | Rust |
There is no single right answer. Embedded and systems code that needs peak performance and control chooses manual or ownership; applications where productivity and safety matter choose GC; those who want safety and performance at once choose ownership. It is a question of what you give up and what you gain.
Pitfalls You Hit in Practice
Let us look at how these concepts show up as real problems in real code.
Dangling pointers and use-after-free. If you return the address of a stack local from a function, that frame is already popped, so the returned address points at garbage. A common C mistake. You must allocate on the heap and return that, or return a copy of the value.
Memory leaks exist in GC languages too. Having a GC does not make leaks impossible. If something is still holding a reference, the GC considers the object alive and will not reclaim it. Stuffing objects into a cache and never evicting them, or registering event listeners and never removing them, keeps references alive and becomes a leak.
The cost of copying large values by value. Value semantics is safe, but passing a huge array or struct into a function by value copies the whole thing and slows down. That is why in C++ you pass large objects by const reference: avoid the copy while preventing mutation.
Do not put large arrays on the stack. The stack is small. Placing a multi-megabyte array as a stack local can overflow the stack all by itself. As a rule, large data belongs on the heap.
Unintended sharing. In a reference-semantics language, if you pass an object and mutate it thinking "it's a copy, so I can change it freely," the original changes too. You must always be conscious of whether you are sharing or copying.
Conclusion
The stack and the heap are not just two words from a textbook; they are the actual mechanism by which a program decides, every moment, where to put a value and when to throw it away. The stack is a fast, automatic, but small and fixed space bound to a function's lifetime; the heap is a flexible but slow space that does not clean up after itself. Pointers bridge the two, value versus reference semantics set the rules of copying, recursion depth tests the stack's limit, and ownership, GC, and manual management take charge of cleaning up the heap.
Once this picture settles into your head, the questions we opened with are no longer mysteries. Why local variables vanish when a function returns, why deep recursion crashes, why "copying" a list mutates the original, why some languages need free and some do not. They are all stories about the stack and the heap and the rules that shuttle values between them. Understanding memory, in the end, is understanding these two spaces and their rules.
References
- Computer Systems: A Programmer's Perspective (CS:APP): https://csapp.cs.cmu.edu/
- The Rust Programming Language — Ownership: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
- What Every Programmer Should Know About Memory (Ulrich Drepper): https://people.freebsd.org/~lstewart/articles/cpumemory.pdf
- Python — sys.setrecursionlimit: https://docs.python.org/3/library/sys.html#sys.setrecursionlimit
- Wikipedia — Call stack: https://en.wikipedia.org/wiki/Call_stack
현재 단락 (1/150)
At some point while learning to program, you hear that "this thing lives on the stack and that one l...