Core Concept: Memory Model
Updated Mar 2026Overview
Rust manages memory through ownership and RAII (Resource Acquisition Is Initialization). No garbage collector, no manual malloc/free. The compiler inserts drop calls at compile time.
Stack vs Heap
Stack Allocation
rust
fn main() {
let x: i32 = 42; // 4 bytes on stack
let y: f64 = 3.14; // 8 bytes on stack
let arr: [u8; 4] = [1, 2, 3, 4]; // 4 bytes on stack
let tup: (i32, bool) = (42, true); // 5 bytes + padding on stack
// All popped from stack when main() returns
}
fn nested() {
let a = 1; // Pushed to stack
{
let b = 2; // Pushed to stack
} // b popped from stack
// a still on stack
} // a popped from stackHeap Allocation
rust
fn main() {
// String: 24 bytes on stack (ptr + len + capacity) → data on heap
let s = String::from("hello");
// Vec: 24 bytes on stack (ptr + len + capacity) → data on heap
let v = vec![1, 2, 3, 4, 5];
// Box: 8 bytes on stack (ptr) → data on heap
let b = Box::new(42);
// Stack layout:
// s: [ptr | 5 | 5] → heap: [h, e, l, l, o]
// v: [ptr | 5 | 5] → heap: [1, 2, 3, 4, 5]
// b: [ptr] → heap: [42]
}RAII and Drop
rust
struct FileHandle {
name: String,
}
impl Drop for FileHandle {
fn drop(&mut self) {
println!("Closing file: {}", self.name);
// Cleanup: close file descriptor, flush buffers, etc.
}
}
fn main() {
let f1 = FileHandle { name: "data.txt".to_string() };
let f2 = FileHandle { name: "log.txt".to_string() };
println!("Files opened");
}
// Output:
// Files opened
// Closing file: log.txt ← f2 dropped first (reverse order)
// Closing file: data.txt ← f1 dropped secondDrop Order
rust
fn main() {
let a = String::from("first"); // Created 1st
let b = String::from("second"); // Created 2nd
let c = String::from("third"); // Created 3rd
// Drop order: c, b, a (reverse creation order)
}
struct Container {
name: String, // Dropped 1st (field order)
data: Vec<i32>, // Dropped 2nd
}
// Fields are dropped in declaration orderEarly Drop
rust
fn main() {
let s = String::from("hello");
drop(s); // Explicitly drop early
// s is now invalid — can't use it
// println!("{s}"); // ERROR
// Useful pattern: release a lock early
let mutex = std::sync::Mutex::new(42);
{
let lock = mutex.lock().unwrap();
// Do critical work
} // Lock released here (MutexGuard dropped)
// Non-critical work continues without holding lock
}Move Semantics and Memory
rust
fn main() {
let s1 = String::from("hello");
// Memory: stack [ptr, len=5, cap=5] → heap [h,e,l,l,o]
let s2 = s1;
// s2 now owns the heap data
// s1 is invalidated — pointer NOT copied (no double free)
// For Copy types, both stack values are valid:
let x: i32 = 42;
let y = x; // Bitwise copy on stack — both valid
}Reference Memory Layout
rust
fn main() {
let s = String::from("hello world");
// Shared reference: 8 bytes (pointer to String on stack)
let r: &String = &s;
// String slice: 16 bytes (pointer + length)
let slice: &str = &s[0..5]; // Points into s's heap data
// Fat pointer: &str is (ptr, len)
// &dyn Trait is (ptr, vtable_ptr)
// &[T] is (ptr, len)
}Zero-Cost Abstractions
rust
// Iterators compile down to the same machine code as manual loops
fn sum_even_doubled(data: &[i32]) -> i32 {
data.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 2)
.sum()
}
// Equivalent to:
fn sum_even_doubled_manual(data: &[i32]) -> i32 {
let mut sum = 0;
for &x in data {
if x % 2 == 0 {
sum += x * 2;
}
}
sum
}
// Both produce identical assembly — zero overheadMemory Layout of Common Types
| Type | Stack Size | Heap Size | Total |
|---|---|---|---|
i32 | 4 bytes | 0 | 4 bytes |
(i32, i32) | 8 bytes | 0 | 8 bytes |
bool | 1 byte | 0 | 1 byte |
char | 4 bytes | 0 | 4 bytes |
&T | 8 bytes | 0 | 8 bytes |
&str | 16 bytes | 0 | 16 bytes (fat ptr) |
String | 24 bytes | N bytes | 24 + N bytes |
Vec<T> | 24 bytes | N * size_of(T) | 24 + data |
Box<T> | 8 bytes | size_of(T) | 8 + T |
Option<&T> | 8 bytes | 0 | 8 bytes (niche) |
Option<Box<T>> | 8 bytes | size_of(T) or 0 | 8 + T or 8 |
Rc<T> | 8 bytes | size_of(T) + 16 | Ref counts + data |
Arc<T> | 8 bytes | size_of(T) + 16 | Atomic ref counts + data |
Niche Optimization
rust
use std::mem::size_of;
// Option<&T> is the same size as &T!
// Compiler uses the null pointer as None
assert_eq!(size_of::<&i32>(), size_of::<Option<&i32>>()); // Both 8 bytes
assert_eq!(size_of::<Box<i32>>(), size_of::<Option<Box<i32>>>());
// Same for NonZero types
use std::num::NonZeroU32;
assert_eq!(size_of::<u32>(), size_of::<Option<NonZeroU32>>()); // Both 4 bytes