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:
| Property | Detail |
|---|---|
| Size | Known at compile time |
| Lifetime | Tied to the enclosing scope |
| Allocation | Automatic (pointer increment) |
| Deallocation | Automatic (pointer decrement) |
| Access | Direct, fast |
| Growth | Not 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:
| Property | Detail |
|---|---|
| Size | Can be determined at runtime |
| Lifetime | Managed by ownership or explicit free |
| Allocation | Explicit (via Box, Vec, String, etc.) |
| Deallocation | Automatic when owner goes out of scope |
| Access | Indirect via pointer |
| Growth | Possible (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.
| Type | Stack | Heap |
|---|---|---|
i32, f64, bool, char | Entire value | — |
(i32, f64) | Entire value | — |
[u8; 32] | Entire value | — |
&str | Pointer + length | The string data |
String | Pointer + length + capacity | The characters |
Vec<T> | Pointer + length + capacity | The elements |
Box<T> | Pointer | The boxed value |
&T | Pointer | The 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
| Aspect | Stack | Heap |
|---|---|---|
| Size | Known at compile time | Determined at runtime |
| Lifetime | Tied to scope | Managed by ownership |
| Allocation | Automatic | Explicit |
| Deallocation | Automatic | When owner goes out of scope |
| Speed | Fast | Slower |
| Growth | Not possible | Possible |
Which Types Live Where
| Type | Location |
|---|---|
i32, f64, bool, char | Stack only |
(i32, f64), [u8; 4] | Stack only |
&str, &T | Stack (pointer), points elsewhere |
String | Stack (ptr/len/cap) + heap (chars) |
Vec<T> | Stack (ptr/len/cap) + heap (elements) |
Box<T> | Stack (pointer) + heap (value) |
Box Use Cases
| Use Case | Example |
|---|---|
| Explicit heap allocation | Box::new(5) |
| Recursive types | Box<List> |
| Large values | Box<LargeData> |
| Trait objects | Box<dyn Trait> |
Stack Sizes (64-bit)
| Type | Size |
|---|---|
i32 | 4 bytes |
f64 | 8 bytes |
bool | 1 byte |
&T | 8 bytes |
&str | 16 bytes (ptr + len) |
String | 24 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Recursive type infinite size | No indirection | Use Box<T> |
| Unexpectedly large stack value | Big array on the stack | Use Box or Vec |
Confusing &str and String | Different stack/heap layout | &str is a reference; String owns |
| Moving a large value copies it | Stack value moved | Box it |
| Stack overflow | Deep recursion | Convert to heap or iteration |
| Cloning when a move suffices | Not tracking ownership | Move 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
| Item | Value |
|---|---|
| Stack | Fixed-size, scoped, fast |
| Heap | Dynamic-size, ownership-managed, slower |
| Stack allocation | Pointer increment |
| Heap allocation | Bookkeeping and pointer dereference |
| Scalar types | Stack only |
String, Vec | Stack part + heap part |
Box<T> | Pointer on stack, value on heap |
| Recursive types | Require Box |
| Large values | Consider Box to avoid copying |
| Trait objects | Box<dyn Trait> |
| Move | Copies stack part, transfers heap ownership |
| Clone | Allocates new heap, copies contents |
| Ownership | Policy 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.
StringandVec<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.Boxprovides a fixed-size pointer that breaks the recursion. - Moves copy the stack part and transfer heap ownership. Moving a
Stringis 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!