Stack and Heap Relationships
In a running Rust program, memory is divided into two primary dynamic regions—the stack and the heap—plus static read-only segments baked into the binary. Rust's ownership system connects the stack and the heap by placing a small, fixed-size owner header on the stack that controls the lifecycle of a dynamically sized buffer on the heap.
Stack vs Heap Characteristics
| Property | The Stack | The Heap |
|---|---|---|
| Allocation mechanism | Automatic LIFO pointer bump (sub sp, #N on function entry) |
Explicit request to the global allocator (malloc / alloc) |
| Deallocation mechanism | Automatic $O(1)$ pointer bump on function/block exit | Freed deterministically when its stack owner's Drop::drop runs |
| Compile-time size requirement | Must have a known, fixed size (Sized) |
Can have any dynamic runtime size or grow/shrink (Vec, String) |
| Access speed & cache locality | Fastest (direct offset from stack pointer; hot in L1 CPU cache) | Slightly slower (requires following a pointer; may cache-miss) |
| Typical capacity | Small (2 MB per worker thread, 8 MB on main thread) |
Limited only by available system RAM |
How Stack Headers Own Heap Buffers (Box<T>, Vec<T>, String)
In Java, Python, or Go, an object variable is just a reference to a heap object managed by a garbage collector. In Rust, every variable lives on the stack by default! When a type like Box<T>, Vec<T>, or String allocates memory on the heap, its fixed-size control header lives on the stack and owns the heap allocation:
Stack (`main` frame) Heap
+----------------------------+
| count: i32 = 42 (4 bytes) | (Lives 100% on the stack; zero heap used)
+----------------------------+
| boxed: Box<i64> (8 bytes) |
| ptr ─────────────────────+──────────> +------------+
+----------------------------+ | 99 | (8 bytes on heap)
| vec: Vec<u8> (24 bytes) | +------------+
| ptr ─────────────────────+──────────> +----+----+----+----+
| len = 3 | | 10 | 20 | 30 | -- | (3 initialized, 1 spare capacity)
| cap = 4 | +----+----+----+----+
+----------------------------+
use std::mem::size_of_val;
fn main() {
let stack_array: [u64; 4] = [1, 2, 3, 4];
let heap_vec: Vec<u64> = vec![1, 2, 3, 4];
println!("Stack array: {} bytes on stack (addr = {:p})", size_of_val(&stack_array), &stack_array);
println!(
"Vec<u64>: {} bytes on stack (header = {:p}), {} bytes on heap (buf = {:p})",
size_of_val(&heap_vec),
&heap_vec,
heap_vec.len() * 8,
heap_vec.as_ptr()
);
}
Stack array: 32 bytes on stack (addr = 0x16fdff1d0)
Vec<u64>: 24 bytes on stack (header = 0x16fdff1b8), 32 bytes on heap (buf = 0x600001718040)
Nested Heap Ownership (Vector of Strings)
When a Vec<String> holds multiple strings, ownership forms a tree of single owners from the stack down through the heap:
- The variable
words: Vec<String>on the stack owns a heap array of 24-byteStringheaders. - Each
Stringheader in that first heap buffer owns its own UTF-8 character buffer on the heap. - When
wordsgoes out of scope on the stack,Droprecursively frees each innerString's UTF-8 buffer first and then frees the outerVecbuffer!