The Stack and the Heap are two memory zones with different rules for storing data.
If you come from languages like JavaScript, Python, or C#, you probably haven’t had to free memory explicitly. A garbage collector detects objects that are no longer used, at the cost of some additional consumption and pauses that depend on each runtime.
In C, we can request memory manually with malloc and free it with free. C++ also allows this, although modern code usually relies on RAII and smart pointers to automate deallocation.
Rust takes a third path. To understand how it achieves safety without a garbage collector, we must first understand where our data lives. In your computer’s RAM, your program primarily uses two areas: the Stack and the Heap.
The stack
The Stack is a LIFO data structure (Last In, First Out).
Imagine a stack of plates in a restaurant.
- When you wash a plate, you put it on top of the stack (Push).
- When you need a plate, you take the one from the top (Pop).
- You cannot take a plate from the middle without causing a mess.
Characteristics of the stack
- Very fast allocation: Usually, just moving the stack pointer is enough to reserve or reclaim space from a call.
- Known size: Values stored directly in a stack frame have a known size at compile time.
- Good locality: Call frames occupy contiguous regions of virtual memory, which usually benefits the processor’s cache.
What is stored here?
The primitive types we saw earlier, whose size never changes:
- Integers (
i32,u64…) - Floats (
f64…) - Booleans (
bool) - Fixed-size arrays (
[i32; 5])
Conceptually, each call creates a frame with its parameters and local variables. When the function ends, that frame becomes unavailable (although the compiler can save some values in registers or optimize them out entirely).
The heap
What if we want to store a list that grows dynamically (like a Vector) or user-entered text (String)? We don’t know how much space it will take at compile time. We can’t put it on the Stack.
For these cases, we use the Heap. The Heap is less organized; it’s like a huge restaurant with many tables.
When you want to put something on the Heap:
- You request a certain amount of memory.
- The memory allocator looks for a free slot large enough and, when necessary, requests more memory from the operating system.
- It marks that slot as “occupied”.
- It returns a pointer (the memory address of that table).
Characteristics of the heap
- Slower to allocate: The system has to search for a slot (bookkeeping).
- Indirect access: To access the data, you must follow a pointer, which can worsen locality and cache performance compared to a directly stored value.
- Dynamic: You can request more memory or free it at runtime.
The connection between stack and heap
In types like String or Vec<T>, the dynamically allocated elements live on the heap, while the structure holding the pointer, length, and capacity is usually part of the local variable.
Imagine the Heap is the restaurant’s dining room and the Stack is the reservation list at the entrance. You (the processor) look at the list (Stack) to know which table (Heap) your diners are at.
Quick comparison
| Feature | Stack | Heap |
|---|---|---|
| Organization | Call frames in LIFO order | Blocks managed by an allocator |
| Speed | Very fast | Slower |
| Data size | Fixed and known | Dynamic and changeable |
| Management cost | Very low | Higher when allocating and freeing |
| Rust example | The value of a local i32 or [i32; 4] | The dynamic content of String, Vec<i32>, or Box<T> |
The cleanup problem
Languages differ significantly in how they handle this. Recovering a stack frame is simple; the interesting problem is knowing when to free each heap allocation.
- In C: The programmer must coordinate calls like
mallocandfree. Forgetting to free an allocation creates a leak; freeing it too early can leave a dangling pointer. - In C++: RAII allows binding resources to the lifetime of objects, although the language still supports manual management and unsafe accesses.
- In Java, Go, or Python: A collector detects unreachable objects and reclaims their memory. It consumes resources and can introduce pauses, though its behavior depends heavily on the implementation.
Rust’s solution: ownership
Rust binds each resource to an owner. When the owner goes out of scope, Rust executes the corresponding drop logic, which for types like String or Vec<T> frees their heap allocation.
No Garbage Collector. No manual free.
fn main() {
{
// 's' is a String.
// The text "hola" is stored on the HEAP.
// The variable 's' (which contains the pointer, length, and capacity) lives on the STACK.
let s = String::from("hola");
// Do things with s...
}
// <--- The scope ends here.
// Rust sees that 's' disappears from the Stack.
// Automatically, Rust calls a special function (drop) that frees the Heap memory.
}This idea, seemingly simple, has profound implications. What happens if we copy s? Can we have two pointers on the Stack pointing to the same place on the Heap?
If we did that and both tried to free the memory at the end… we’d have a Double Free error. Rust prevents this through the Ownership system.
Why is it important to know this?
Understanding Stack vs Heap is important in Rust because the language forces you to decide where to put things.
- If you use
let x = 5;, it’s cheap and simple (Stack). - If you use
let x = Box::new(5);, you allocate that5on the Heap (with an allocation cost, and until its owner is destroyed). - If you copy data from the Stack, it’s fast (copying bits).
- If you clone a collection with data on the Heap, you typically need to allocate memory and copy its elements.