Memory management is at the heart of Rust's design. Unlike languages with garbage collectors (Java, Go, Python) that automate heap management, and unlike C/C++ where you manually call malloc/free or new/delete, Rust uses a third approach: compile-time ownership rules that guarantee correctness without any runtime overhead.
To understand Rust's memory model, you first need to understand the two fundamental memory regions: the stack and the heap.
The Stack
The stack is a region of memory organized as a last-in, first-out (LIFO) data structure. It is the default place where local variables in Rust (and most languages) live.
Stack Memory Layout (grows downward):
+---------------------------+ High address
| main() frame |
| +---------------------+ |
| | local variables | |
| | function args | |
| +---------------------+ |
| foo() frame |
| +---------------------+ |
| | local variables | | <-- Stack Pointer
| +---------------------+ |
| (free space) |
+---------------------------+ Low address
Stack Properties
| Property | Details |
|---|---|
| Allocation | Automatic, by the CPU |
| Deallocation | Automatic, when scope ends |
| Speed | Extremely fast (just move a pointer) |
| Size | Fixed and known at compile time |
| Lifetime | Tied to the enclosing scope |
| Location | RAM |
What Goes on the Stack
- Local variables in functions
- Function arguments (when passed by value)
- Fixed-size data (integers, floats, booleans, characters, fixed arrays)
fn foo() {
let a: i32 = 10; // lives on the stack
let b: f64 = 3.14; // lives on the stack
} // a and b are automatically freed here
fn main() {
foo();
// After foo() returns, its stack frame is gone
}
The key advantage of the stack: allocation and deallocation are essentially free. The CPU just adjusts a single pointer (the stack pointer) to allocate or free an entire frame.
Stack Size Limitations
Because the stack size is fixed (typically 8 MB on Linux/macOS), you cannot store large data on it. Arrays of millions of elements, dynamic collections, or data whose size is not known at compile time must go on the heap.
The Heap
The heap is a much larger, but less structured region of memory. It is used for dynamic memory allocation — data whose size is not known at compile time or that needs to outlive the function that created it.
Heap Memory Layout:
+------------------------------------------+
| Heap (managed by allocator) |
| |
| [used block] [free] [used] [free] ... |
| |
| Allocation: find free block, mark used |
| Deallocation: mark block as free |
+------------------------------------------+
Heap Properties
| Property | Details |
|---|---|
| Allocation | Explicit, via allocator |
| Deallocation | Managed by Rust's ownership system |
| Speed | Slower (allocator must find free space) |
| Size | Dynamic, limited by available RAM |
| Lifetime | Controlled by the owner |
| Location | RAM |
Heap Allocation with `Box`
In Rust, the primary way to place a value on the heap is with Box<T>. A Box is a smart pointer that:
- Allocates the value on the heap
- Stores a pointer to it on the stack
- Automatically deallocates the heap memory when the
Boxgoes out of scope
fn main() {
let y = Box::new(10);
// Stack: y (a pointer, ~8 bytes)
// Heap: 10 (i32, 4 bytes)
println!("y = {}", *y); // dereference with * to get the value
}
// y goes out of scope here, Box automatically frees the heap memory
Stack and Heap with Box:
Stack: Heap:
+----------+ +----------+
| y (ptr) | ----> | 10 (i32) |
+----------+ +----------+
The y syntax dereferences the Box to access the underlying value. Rust's smart pointers implement Deref, so in many contexts you do not need to write explicitly.
When to Use Box
- When you have a type whose size cannot be known at compile time (recursive types)
- When you have large data and want to transfer ownership without copying
- When you need a value that lives longer than the current stack frame
// Recursive types require Box (their size would be infinite without it)
enum List {
Cons(i32, Box<List>),
Nil,
}
Vectors: Heap-Backed Dynamic Arrays
The Vec<T> type is one of the most commonly used types in Rust. A vector stores its elements on the heap while keeping metadata (pointer, length, capacity) on the stack:
fn main() {
let z = vec![1, 2, 3];
// Stack: z = { ptr, len: 3, capacity: 3 }
// Heap: [1, 2, 3]
}
// When z goes out of scope, the heap memory is freed automatically
Vec Layout:
Stack: Heap:
+--------------------+ +-------+-------+-------+
| ptr | len | cap | --> | 1 | 2 | 3 |
+--------------------+ +-------+-------+-------+
The vector owns its heap-allocated buffer. When the vector is dropped, the buffer is freed. No explicit deallocation is needed.
Rust's Ownership: Connecting Stack and Heap
Rust's ownership system is designed precisely around this stack/heap distinction:
Ownership Rules:
1. Every heap allocation has exactly one owner.
2. When the owner goes out of scope, the heap memory is freed.
3. Ownership can be transferred (moved), but not copied by default.
This is how Rust achieves memory safety without a garbage collector: every heap allocation has a statically determined point in the code where it will be freed, enforced by the compiler.
fn main() {
let v1 = vec![1, 2, 3]; // v1 owns the heap data
let v2 = v1; // ownership moves to v2
// println!("{:?}", v1); // ERROR: v1 no longer owns the data
println!("{:?}", v2); // OK: v2 is the owner
}
Stack vs. Heap at a Glance
+---------------------+----------------------------+----------------------------+
| Property | Stack | Heap |
+---------------------+----------------------------+----------------------------+
| Allocation speed | Extremely fast | Slower (allocator work) |
| Deallocation | Automatic (scope exit) | Automatic (ownership drop) |
| Size | Small, compile-time known | Large, runtime dynamic |
| Lifetime | Tied to scope | Controlled by owner |
| Access speed | Fast (cache-friendly) | Slightly slower |
| Typical use | Local variables, primitives| Dynamic collections, Boxes |
+---------------------+----------------------------+----------------------------+
Practical Implications
Understanding stack vs. heap has direct practical consequences:
Prefer Stack When Possible
Stack allocations are cheaper and more cache-friendly. If your data has a fixed, known size, keep it on the stack:
// Prefer this (stack):
let buffer: [u8; 1024] = [0; 1024];
// Over this (heap) when size is known:
let buffer: Vec<u8> = vec![0; 1024];
Use Heap for Dynamic or Large Data
When data size is not known at compile time, or when you need to share data across function boundaries without copying, use heap types:
fn load_data() -> Vec<u8> {
vec![1, 2, 3, 4, 5] // size determined at runtime
}
The Clone Trap
Cloning heap-allocated data creates a full copy on the heap, which is expensive. Understand when you need a clone vs. a reference:
let v1 = vec![1, 2, 3];
let v2 = v1.clone(); // copies all heap data — potentially expensive
let v3 = &v1; // borrows — no heap allocation
A Complete Example
fn demonstrate_stack() {
let a: i32 = 10; // stack
let b: f64 = 3.14; // stack
println!("Stack values: a = {}, b = {}", a, b);
}
fn demonstrate_heap() {
// Box: single value on heap
let boxed = Box::new(42);
println!("Boxed value: {}", *boxed);
// Vec: array on heap
let mut data = vec![1, 2, 3];
data.push(4);
println!("Vec: {:?}", data);
}
fn main() {
demonstrate_stack();
demonstrate_heap();
// All heap memory freed automatically here
}
Conclusion
Rust's memory model is a masterclass in clarity. The stack is fast, automatic, and scope-bound. The heap is flexible and dynamic, managed not by a garbage collector but by Rust's ownership system at compile time. Box<T> gives you explicit heap allocation with a clean, owning API. Vec<T> and other standard library collections build on the same foundation.
By understanding the stack and heap deeply, you are ready to understand Rust's borrowing and lifetime system — the next layer of Rust's memory safety guarantees that builds directly on these concepts.