Ownership

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:

  1. The variable words: Vec<String> on the stack owns a heap array of 24-byte String headers.
  2. Each String header in that first heap buffer owns its own UTF-8 character buffer on the heap.
  3. When words goes out of scope on the stack, Drop recursively frees each inner String's UTF-8 buffer first and then frees the outer Vec buffer!