In a running Rust program, stack and heap are two distinct regions of a process’s virtual address space, each with different performance, lifetime, and allocation semantics. Rust’s ownership system and Drop
trait provide deterministic destruction of both stack and heap data, without requiring a garbage collector. Allocation itself may be explicit or implicit, but destruction is always predictable.
Stack
The stack is a contiguous, fixed-size region of memory allocated per thread. It grows and shrinks in a Last-In, First-Out (LIFO) manner as functions are called and return. Each function call creates a stack frame, holding:
- Parameters and local variables (only those with a
Sized
type) - Saved registers and the return address
- Optional spill slots and alignment padding
- Return values (unless small scalars are returned in registers via the ABI)
- Metadata for fat pointers (length for slices, vtable for trait objects)
Allocation on the stack is effectively a pointer bump—constant time, cache-friendly, and very fast. This speed comes at the cost of limited size (commonly ~8 MB for the main thread, ~2 MB for spawned Rust threads by default). The stack ends with a guard page; exceeding it triggers stack overflow, which in Rust aborts the process after printing an error. Unwinding from stack overflow is not supported.
Because stack memory must be sized at compile time, it cannot directly store dynamically sized data like str
or [T]
. Instead, it stores pointers (plus metadata) to such data elsewhere, often on the heap. Large fixed-size data (like [u8; 1_000_000]
) can live on the stack, but risks overflow—better to store it in the heap.
Best practices:
- Use iteration instead of deep recursion; Rust has no guaranteed tail call optimization.
- Avoid placing large data directly on the stack—allocate it on the heap.
- Use
std::thread::Builder::stack_size
if you must increase thread stack size.
Heap
The heap is a dynamically managed pool of memory for values whose size or lifetime is not known at compile time. The OS supplies memory pages, but a user-space allocator (e.g., system allocator, jemalloc
, mimalloc
) manages requests within that space. Allocation is slower than stack allocation because it involves bookkeeping and may require OS interaction, though in steady-state use, accessing heap data already in cache is as fast as accessing stack data.
In Rust, heap allocation is done through standard library types:
Box<T>
: owns a single heap-allocatedT
Vec<T>
/String
: store a pointer, length, and capacity on the stack; buffer lives in the heapHashMap<K, V>
: owns heap memory for bucketsRc<T>
/Arc<T>
: reference-counted pointers to heap-allocated data (Arc
is thread-safe via atomics)
When a heap allocation fails, Rust calls handle_alloc_error
, which panics (aborts or unwinds depending on panic strategy). For recoverable allocation attempts, use methods like try_reserve
. Heap memory is freed when its owner is dropped.
Risks & performance notes:
- Leaks: Safe Rust can leak via
Rc
/Arc
cycles—break cycles withWeak
. - Fragmentation: Both external (free space too fragmented) and internal (wasted space inside blocks) can occur; mitigated with arenas (
bumpalo
), pooling, orVec::with_capacity
. - Bottlenecks: Frequent small allocations can degrade performance—reuse allocations where possible.
Lifetime Differences
- Stack: Data lives until the end of its scope or until the stack frame is popped.
- Heap: Data lives until the last owner is dropped, which may outlive the scope in which it was allocated.
Summary Table
Property | Stack | Heap |
---|---|---|
Size | Fixed per thread (~MBs) | Limited by address space and quotas |
Allocation | Constant-time pointer bump | Allocator bookkeeping + possible OS call |
Access speed | Very fast, cache-friendly | Slightly slower due to indirection |
Lifetime control | Scope-based (frame pop / early drop) | Owner drop (Drop trait) |
Stores | Sized types, pointers, metadata | Dynamically sized and large data |
Failure mode | Stack overflow → abort | OOM → panic (handle_alloc_error ) |
Closing Notes
Rust’s safety guarantees extend to both regions: no use-after-free, no invalid pointer dereference, no double free—in safe code. However, efficiency comes from choosing the right storage for the right data. A senior Rust developer should master not just what stack and heap are, but also the trade-offs in performance, safety, and architecture when deciding where data should live.