| |

Rust 11 🦀 Memory Layout: Stack vs Heap Storage

Every value in a running program lives somewhere in memory. Rust programs use two regions for this: the stack and the heap. The stack is fast, automatically managed, and stores data whose size is known at compile time. The heap is slower, explicitly managed, and stores data whose size can change at runtime. Understanding which values go where — and why — is the foundation for understanding ownership, borrowing, and the performance characteristics of Rust code.

The distinction is not academic. It explains why String is different from &str, why moving a String is cheap while cloning it is expensive, why Box<T> exists, and why recursive data structures require heap allocation. The stack and the heap are not implementation details hidden by the language; they are part of the mental model that Rust programmers use to reason about memory. Ownership rules exist precisely because heap allocation needs a policy for when memory is freed, and the stack needs no such policy because its lifetime is tied to scope.

This chapter covers the stack, the heap, the difference between them, which types live where, the Box<T> type, and the patterns that follow from understanding memory layout.

Key point: The stack stores fixed-size values with automatic lifetime tied to scope. The heap stores values whose size is known only at runtime, and requires explicit allocation and deallocation. Rust’s ownership system manages heap memory automatically: when the owner goes out of scope, the heap allocation is freed. Box<T> is the simplest heap allocation, a pointer to a value on the heap.


Why stack and heap exist

The size problem. Some values have a size known at compile time. An i32 is four bytes, a bool is one byte, a tuple of three i32s is twelve bytes. These values can be placed directly on the stack, and the compiler knows exactly how much space to reserve. Other values have a size that depends on runtime data. A String can be any length, and its content is not known until the program runs. It cannot be stored entirely on the stack because the compiler cannot reserve a fixed amount of space.

The lifetime problem. Stack values are tied to the function call that creates them. When the function returns, its stack frame is popped, and the values are gone. This is automatic and free. Heap values have no such automatic lifetime; they persist until they are explicitly freed, which in Rust means until their owner goes out of scope.

The performance problem. Stack allocation is a single pointer increment. Heap allocation requires finding a free block, updating bookkeeping structures, and possibly calling into the operating system. Stack access is direct because the location is known at compile time. Heap access requires dereferencing a pointer, which may involve a cache miss.

The flexibility problem. Heap values can grow and shrink. A Vec<T> starts empty and adds elements, reallocating as it grows. This flexibility is not possible on the stack, where the size is fixed at the moment of allocation.

The ownership problem. Heap allocation requires a policy: who frees the memory, and when? C requires manual free. Java and Go use garbage collection. Rust uses ownership: the variable that owns the heap value frees it when it goes out of scope. This is the entire reason ownership exists.


a. The stack

The stack is a region of memory organized as a last-in, first-out structure. Every function call pushes a frame onto the stack containing the function’s parameters, local variables, and return address. When the function returns, the frame is popped.

fn add(a: i32, b: i32) -> i32 {
    let sum = a + b;
    sum
}

When add is called, the stack frame contains a, b, and sum, each four bytes. The total is known at compile time, so the compiler reserves exactly that much space. When add returns, the frame is popped in a single operation.

Values on the stack have these properties:

PropertyDetail
SizeKnown at compile time
LifetimeTied to the enclosing scope
AllocationAutomatic (pointer increment)
DeallocationAutomatic (pointer decrement)
AccessDirect, fast
GrowthNot possible

Types that live entirely on the stack include all scalar types (i32, f64, bool, char), tuples and arrays of fixed size, and structs whose fields are all stack types.

let x: i32 = 42;                    // stack
let point: (i32, i32) = (1, 2);     // stack
let arr: [u8; 4] = [1, 2, 3, 4];    // stack
struct Pair { a: i32, b: i32 }      // stack if the value is on the stack

The stack is fast because the compiler knows the layout at compile time and the CPU’s stack pointer handles allocation and deallocation.


b. The heap

The heap is a region of memory used for dynamic allocation. When a value’s size is not known at compile time, or when it needs to outlive the current scope, it is placed on the heap and accessed through a pointer stored on the stack.

let s = String::from("hello");

The String value itself is a small structure on the stack containing three fields: a pointer to the heap, a length, and a capacity. The actual characters h, e, l, l, o live on the heap. The stack part is fixed-size (typically 24 bytes on a 64-bit system), and the heap part is variable.

Values on the heap have these properties:

PropertyDetail
SizeCan be determined at runtime
LifetimeManaged by ownership or explicit free
AllocationExplicit (via Box, Vec, String, etc.)
DeallocationAutomatic when owner goes out of scope
AccessIndirect via pointer
GrowthPossible (via reallocation)

The heap is slower because allocation involves bookkeeping and access requires dereferencing a pointer, which can miss the CPU cache.


c. Which types live where

The question “is this type on the stack or the heap?” is not about the type alone but about the value. Some types are always on the stack, some always involve the heap, and some are on the stack but point to the heap.

TypeStackHeap
i32, f64, bool, charEntire value—
(i32, f64)Entire value—
[u8; 32]Entire value—
&strPointer + lengthThe string data
StringPointer + length + capacityThe characters
Vec<T>Pointer + length + capacityThe elements
Box<T>PointerThe boxed value
&TPointerThe referenced value

A &str is a slice reference: a pointer to a string and a length. The pointer and length are on the stack; the string data they point to may be in the binary’s read-only memory (for literals), on the stack (for a local array), or on the heap (for a String). The reference itself is always a fixed-size stack value.

A String is a growable UTF-8 buffer. Its stack part is three words: a pointer to heap memory, a length, and a capacity. The heap part holds the characters, and the capacity may be larger than the length to allow growth without reallocation on every push.

A Vec<T> is the same structure: a pointer, a length, and a capacity, with the elements on the heap.


d. Box: explicit heap allocation

Box<T> is the simplest way to put a value on the heap. It allocates space for a T on the heap and stores a pointer to it on the stack.

let b = Box::new(5);
println!("{}", b);       // 5
println!("{}", *b);      // 5 (dereference)

The Box itself is a stack value: a pointer. The 5 is on the heap. When b goes out of scope, the heap allocation is freed.

Box is used in three situations:

When the size is not known at compile time. A recursive type like a linked list needs Box to break the cycle of a type containing itself:

enum List {
    Cons(i32, Box<List>),
    Nil,
}

let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));

Without Box, the compiler would try to compute the size of List, which contains List, which contains List — an infinite size. Box is a pointer, whose size is known, so the recursion terminates.

When the value is large. Moving a large stack value copies it. Putting it in a Box moves only the pointer:

struct LargeData {
    buffer: [u8; 1_000_000],
}

let boxed = Box::new(LargeData { buffer: [0; 1_000_000] });
// Only the pointer is moved, not the million bytes

When you need a trait object. A Box<dyn Trait> allows a heap-allocated value of any type implementing the trait:

trait Shape {
    fn area(&self) -> f64;
}

let shapes: Vec<Box<dyn Shape>> = vec![
    Box::new(Circle { radius: 1.0 }),
    Box::new(Square { side: 2.0 }),
];

A Vec<Box<dyn Shape>> can hold different concrete types because each is behind a pointer. Without Box, a Vec must hold values of the same type.


e. Stack and heap in function calls

When a function is called, its parameters and local variables are placed on the stack. If a parameter is a String, the stack part (pointer, length, capacity) is moved into the function’s frame, and the heap part stays where it is.

fn takes_string(s: String) {
    println!("{}", s);
}

let s = String::from("hello");
takes_string(s);
// s is moved; the stack part is copied into the function frame,
// the heap pointer is transferred, and the function frees it on return.

Returning a value from a function moves it out of the function’s frame. For a stack value, the value is copied. For a heap value, the stack part is copied and the heap part remains, with the ownership transferring to the caller.

fn create_string() -> String {
    String::from("hello")
}

let s = create_string();
// The String's stack part is returned to the caller,
// and the heap allocation persists.

The heap allocation is freed only when the final owner goes out of scope. In the example, that is when s goes out of scope in the caller.


f. Why this matters for ownership

Ownership is a policy for heap memory. The stack needs no policy because it is scoped. The heap needs a rule about who frees the memory and when. Rust’s rule is: the variable that owns the value frees it when it goes out of scope.

{
    let s = String::from("hello");
    // s owns the heap allocation
} // s goes out of scope, the heap allocation is freed

The stack part of s is popped automatically. The heap part is freed by the destructor that Rust inserts when the scope ends. This is the mechanism that prevents memory leaks and use-after-free without a garbage collector.

When ownership moves, the responsibility for freeing moves with it. The original variable no longer frees the heap, because it no longer owns it.

let s1 = String::from("hello");
let s2 = s1;
// s1 no longer owns the heap; s2 does.
// s1 is now invalid.

This is why moving a String is cheap: only the stack part is copied, and the heap part stays where it is, with a new owner. Cloning, by contrast, allocates a new heap block and copies the contents, which is why it is more expensive.


Complete Example Session

// ============================================
// PART 1: STACK VALUES
// ============================================
fn main() {
    let x: i32 = 42;
    let point: (i32, i32) = (1, 2);
    let arr: [u8; 4] = [1, 2, 3, 4];

    println!("{} {:?} {:?}", x, point, arr);
    // All values live entirely on the stack.
}
// ============================================
// PART 2: HEAP VALUES
// ============================================
fn main() {
    let s = String::from("hello");
    let v = vec![1, 2, 3];

    println!("{} {:?}", s, v);
    // The stack holds pointer/length/capacity;
    // the heap holds the contents.
}
// ============================================
// PART 3: STACK SIZE OF A STRING
// ============================================
fn main() {
    let s = String::from("hello");
    println!("size of String: {} bytes", std::mem::size_of::<String>());
    println!("size of &str:   {} bytes", std::mem::size_of::<&str>());
    println!("size of i32:    {} bytes", std::mem::size_of::<i32>());
    // String: 24 bytes (ptr + len + cap)
    // &str:   16 bytes (ptr + len)
    // i32:     4 bytes
}
// ============================================
// PART 4: BOX FOR EXPLICIT HEAP ALLOCATION
// ============================================
fn main() {
    let b = Box::new(5);
    println!("{}", b);     // 5
    println!("{}", *b);    // 5
    // b is a pointer on the stack; 5 is on the heap.
}
// ============================================
// PART 5: BOX FOR RECURSIVE TYPES
// ============================================
enum List {
    Cons(i32, Box<List>),
    Nil,
}

fn main() {
    let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
    // Box breaks the infinite size of the recursive type.
}
// ============================================
// PART 6: BOX FOR LARGE VALUES
// ============================================
struct LargeData {
    buffer: [u8; 1_000_000],
}

fn main() {
    let boxed = Box::new(LargeData { buffer: [0; 1_000_000] });
    // Only the pointer is moved, not the million bytes.
}
// ============================================
// PART 7: BOX FOR TRAIT OBJECTS
// ============================================
trait Shape {
    fn area(&self) -> f64;
}

struct Circle { radius: f64 }
struct Square { side: f64 }

impl Shape for Circle {
    fn area(&self) -> f64 { 3.14159 * self.radius * self.radius }
}

impl Shape for Square {
    fn area(&self) -> f64 { self.side * self.side }
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { radius: 1.0 }),
        Box::new(Square { side: 2.0 }),
    ];
    for shape in &shapes {
        println!("{}", shape.area());
    }
}
// ============================================
// PART 8: MOVE VERSUS CLONE
// ============================================
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // move: stack part copied, heap shared

    let s3 = s2.clone();  // clone: new heap allocation

    println!("{} {}", s2, s3);
    // s1 is no longer valid.
}
// ============================================
// PART 9: STACK FRAMES
// ============================================
fn inner(x: i32) -> i32 {
    let y = x * 2;   // y is on the stack frame of inner
    y
}

fn outer() -> i32 {
    let a = 5;       // a is on the stack frame of outer
    inner(a)         // the return value is copied to outer's frame
}

fn main() {
    let result = outer();
    println!("{}", result);
    // Each call pushes a frame; each return pops it.
}
// ============================================
// PART 10: OWNERSHIP FREES THE HEAP
// ============================================
fn main() {
    {
        let s = String::from("scoped");
        println!("{}", s);
    } // s goes out of scope; the heap allocation is freed

    // println!("{}", s);  // Error: s is out of scope
}

These ten parts cover stack values, heap values, stack size of common types, Box for explicit allocation, Box for recursive types, Box for large values, Box for trait objects, move versus clone, stack frames in function calls, and ownership freeing the heap.


Quick Reference

Stack vs Heap

AspectStackHeap
SizeKnown at compile timeDetermined at runtime
LifetimeTied to scopeManaged by ownership
AllocationAutomaticExplicit
DeallocationAutomaticWhen owner goes out of scope
SpeedFastSlower
GrowthNot possiblePossible

Which Types Live Where

TypeLocation
i32, f64, bool, charStack only
(i32, f64), [u8; 4]Stack only
&str, &TStack (pointer), points elsewhere
StringStack (ptr/len/cap) + heap (chars)
Vec<T>Stack (ptr/len/cap) + heap (elements)
Box<T>Stack (pointer) + heap (value)

Box Use Cases

Use CaseExample
Explicit heap allocationBox::new(5)
Recursive typesBox<List>
Large valuesBox<LargeData>
Trait objectsBox<dyn Trait>

Stack Sizes (64-bit)

TypeSize
i324 bytes
f648 bytes
bool1 byte
&T8 bytes
&str16 bytes (ptr + len)
String24 bytes (ptr + len + cap)
Vec<T>24 bytes (ptr + len + cap)
Box<T>8 bytes (ptr)

Best Practices

✅ Do This:

// Let small fixed-size values live on the stack
let x = 42;
let point = (1, 2);

// Use Box for recursive types
enum List { Cons(i32, Box<List>), Nil }

// Use Box for large values to avoid copying
let large = Box::new(LargeData { ... });

// Use Box<dyn Trait> for heterogeneous collections
let shapes: Vec<Box<dyn Shape>> = vec![...];

// Rely on ownership to free the heap
{
    let s = String::from("scoped");
} // freed here

❌ Don’t Do This:

// Unnecessary Box for small values
let x = Box::new(5);  // ❌ stack would do

// Recursive type without Box
enum List { Cons(i32, List), Nil }  // ❌ infinite size

// Manual free (not available in safe Rust)
// free(ptr);  // ❌ ownership handles it

// Cloning when a move would do
let s2 = s1.clone();  // ❌ if s1 is not used after

Common Pitfalls

PitfallWhy It HappensFix
Recursive type infinite sizeNo indirectionUse Box<T>
Unexpectedly large stack valueBig array on the stackUse Box or Vec
Confusing &str and StringDifferent stack/heap layout&str is a reference; String owns
Moving a large value copies itStack value movedBox it
Stack overflowDeep recursionConvert to heap or iteration
Cloning when a move sufficesNot tracking ownershipMove when the source is not reused

Real-World Examples

1. Stack-Only Struct

struct Point { x: i32, y: i32 }

2. Heap-Allocated String

let s = String::from("hello");

3. Recursive Enum

enum Tree {
    Leaf(i32),
    Node(Box<Tree>, Box<Tree>),
}

4. Box for Large Array

let buffer = Box::new([0u8; 1_000_000]);

5. Trait Object

let handler: Box<dyn Fn()> = Box::new(|| println!("hi"));

6. Vec of Boxes

let items: Vec<Box<dyn Display>> = vec![...];

7. Move Semantics

let s1 = String::from("hello");
let s2 = s1;  // move

8. Clone for Deep Copy

let s2 = s1.clone();  // new heap allocation

9. Scope Frees Heap

{
    let v = vec![1, 2, 3];
} // v freed

10. Size Check

println!("{}", std::mem::size_of::<String>());

Visual

Stack and Heap

┌──────────────────────────────────────────────────────────────┐
│  STACK                          HEAP                         │
│  ┌─────────────────────┐        ┌─────────────────────┐      │
│  │  main frame         │        │                     │      │
│  │  ┌───────────────┐  │        │  ┌───────────────┐  │      │
│  │  │ x: i32 = 42   │  │        │  │ "hello"       │  │      │
│  │  └───────────────┘  │        │  │ h e l l o     │  │      │
│  │  ┌───────────────┐  │        │  └───────────────┘  │      │
│  │  │ s: String     │  │        │                     │      │
│  │  │  ptr ─────────┼──┼────────▶                     │      │
│  │  │  len: 5       │  │        │                     │      │
│  │  │  cap: 5       │  │        │                     │      │
│  │  └───────────────┘  │        │                     │      │
│  └─────────────────────┘        └─────────────────────┘      │
└──────────────────────────────────────────────────────────────┘

Stack Frame in Function Calls

┌──────────────────────────────────────────────────────────────┐
│  main calls outer:                                           │
│  ┌─────────────────────┐                                     │
│  │  main frame         │                                     │
│  ├─────────────────────┤                                     │
│  │  outer frame        │  ← pushed                           │
│  │  a: i32 = 5         │                                     │
│  └─────────────────────┘                                     │
│                                                              │
│  outer calls inner:                                          │
│  ┌─────────────────────┐                                     │
│  │  main frame         │                                     │
│  ├─────────────────────┤                                     │
│  │  outer frame        │                                     │
│  │  a: i32 = 5         │                                     │
│  ├─────────────────────┤                                     │
│  │  inner frame        │  ← pushed                           │
│  │  x: i32 = 5         │                                     │
│  │  y: i32 = 10        │                                     │
│  └─────────────────────┘                                     │
│                                                              │
│  inner returns → frame popped                                │
│  outer returns → frame popped                                │
└──────────────────────────────────────────────────────────────┘

Box Allocation

┌──────────────────────────────────────────────────────────────┐
│  let b = Box::new(5);                                        │
│                                                              │
│  STACK                          HEAP                         │
│  ┌─────────────────────┐        ┌─────────────────────┐      │
│  │  b: Box<i32>        │        │  5                  │      │
│  │  ptr ───────────────┼───────▶│                     │      │
│  └─────────────────────┘        └─────────────────────┘      │
│                                                              │
│  Box is a pointer on the stack; the value is on the heap.    │
│  When b goes out of scope, the heap allocation is freed.     │
└──────────────────────────────────────────────────────────────┘

Recursive Type with Box

┌──────────────────────────────────────────────────────────────┐
│  enum List {                                                 │
│    Cons(i32, Box<List>),                                     │
│    Nil,                                                      │
│  }                                                           │
│                                                              │
│  Cons(1, Box ────▶ Cons(2, Box ────▶ Cons(3, Box ────▶ Nil)) │
│                                                              │
│  Without Box, the type would be infinite in size because     │
│  List contains List contains List...                         │
│  Box provides a fixed-size pointer, terminating the recursion.│
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
StackFixed-size, scoped, fast
HeapDynamic-size, ownership-managed, slower
Stack allocationPointer increment
Heap allocationBookkeeping and pointer dereference
Scalar typesStack only
String, VecStack part + heap part
Box<T>Pointer on stack, value on heap
Recursive typesRequire Box
Large valuesConsider Box to avoid copying
Trait objectsBox<dyn Trait>
MoveCopies stack part, transfers heap ownership
CloneAllocates new heap, copies contents
OwnershipPolicy for freeing heap memory

Key takeaways:

  • The stack stores fixed-size values with scoped lifetimes. Allocation and deallocation are automatic and fast. Scalar types and fixed-size aggregates live entirely on the stack.
  • The heap stores values whose size is determined at runtime. String and Vec<T> have a stack part (pointer, length, capacity) and a heap part (the contents). Access is indirect through the pointer.
  • Box<T> is explicit heap allocation. It stores a pointer on the stack and the value on the heap. It is used for recursive types, large values, and trait objects.
  • Recursive types require Box. A type that contains itself would have infinite size. Box provides a fixed-size pointer that breaks the recursion.
  • Moves copy the stack part and transfer heap ownership. Moving a String is cheap: only the pointer, length, and capacity are copied, and the heap allocation stays where it is.
  • Cloning allocates new heap memory. clone() copies the contents to a new heap block. It is more expensive than a move and should be used only when the original is still needed.
  • Ownership is the policy for freeing heap memory. When the owner goes out of scope, the heap allocation is freed. This is why ownership exists: the stack needs no policy, but the heap does.
  • Understanding memory layout explains ownership. Every ownership rule follows from the fact that heap allocation needs a policy for deallocation, and the stack does not.

Remember: Memory layout is the foundation on which Rust’s ownership system is built. The stack handles fixed-size, scoped values automatically. The heap handles dynamic-size values with an explicit policy: the owner frees them when it goes out of scope. String, Vec, and Box all have a stack part that points to a heap part. Moving them copies only the stack part; cloning copies both. Recursive types need Box because a type cannot contain itself without a pointer. Understanding this layout makes ownership intuitive: the rules exist because the heap needs them, and the stack does not. When you reason about whether a value is moved or copied, whether a type needs Box, or why a String and a &str behave differently, the answer is always in the memory layout.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!