Skip to content

Core Concept: Memory Model

Updated Mar 2026

Overview

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 stack

Heap 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 second

Drop 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 order

Early 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 overhead

Memory Layout of Common Types

TypeStack SizeHeap SizeTotal
i324 bytes04 bytes
(i32, i32)8 bytes08 bytes
bool1 byte01 byte
char4 bytes04 bytes
&T8 bytes08 bytes
&str16 bytes016 bytes (fat ptr)
String24 bytesN bytes24 + N bytes
Vec<T>24 bytesN * size_of(T)24 + data
Box<T>8 bytessize_of(T)8 + T
Option<&T>8 bytes08 bytes (niche)
Option<Box<T>>8 bytessize_of(T) or 08 + T or 8
Rc<T>8 bytessize_of(T) + 16Ref counts + data
Arc<T>8 bytessize_of(T) + 16Atomic 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

Built with VitePress | Rust SOP Documentation