RUST: Stack and Heap Memory in Rust

Rust gives programmers explicit, safe control over both stack and heap memory. Understanding how these two memory regions work — and how Rust's ownership system bridges them — is essential for writing efficient, zero-overhead Rust code.

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.

text
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

PropertyDetails
AllocationAutomatic, by the CPU
DeallocationAutomatic, when scope ends
SpeedExtremely fast (just move a pointer)
SizeFixed and known at compile time
LifetimeTied to the enclosing scope
LocationRAM

What Goes on the Stack

  • Local variables in functions
  • Function arguments (when passed by value)
  • Fixed-size data (integers, floats, booleans, characters, fixed arrays)

rust
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.

text
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

PropertyDetails
AllocationExplicit, via allocator
DeallocationManaged by Rust's ownership system
SpeedSlower (allocator must find free space)
SizeDynamic, limited by available RAM
LifetimeControlled by the owner
LocationRAM

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:

  1. Allocates the value on the heap
  2. Stores a pointer to it on the stack
  3. Automatically deallocates the heap memory when the Box goes out of scope

rust
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

text
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

rust
// 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:

rust
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

text
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:

text
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.

rust
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

text
+---------------------+----------------------------+----------------------------+
| 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:

rust
// 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:

rust
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:

rust
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

rust
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.