Understanding Rust's Memory Model

4 months ago 3

07 Jul, 2025

So I have been working with garbage collected programming languages mostly. I have written decent amount of C++ but that was during my uni days when I was solving LeetCode problems and college assignments. Recently I started learning Rust and here is my understanding of what’s happening beneath the abstractions.

You can see this as a blog where I guide you through what’s memory and how memory is allocated in Rust. This is what I understood so if you think I’ve written something wrong or can be improved please dm me on X/Twitter.

Ok so consider memory as a massive array where each element can hold 8 bits of data. This data is nothing but an address.

Address | Value (in binary) ---------|------------------ 0x1000 | 01110011 (115 in decimal, 's' in ASCII) 0x1001 | 01101100 (108 in decimal, 'l' in ASCII) 0x1002 | 01101111 (111 in decimal, 'o' in ASCII) 0x1003 | 01110000 (112 in decimal, 'p' in ASCII)

So stack works like you can only add/push or remove/pop from the top. It is extremely fast because the CPU just needs to move a pointer up or down.

When we create a variable in Rust (like below) the all go to the stack memory:

fn main() { let x: i32 = 100; let flag: bool = true; let point: f64 = 0.5; }

And here is how the memory looks like:

Stack +---------------------+ <-- Top of Stack (higher address) | point = 0.5 | f64 (8 bytes) +---------------------+ | flag = true | bool (1 byte + padding) +---------------------+ | x = 100 | i32 (4 bytes) +---------------------+ <-- Bottom (lowest address for this frame)

Some things to note here:

  • Each variable is stored top to down as we declare them but the stack grows downwards so the point is pushed last but appears at the top in memory
  • The rust compiler optimizes layout for performance so the actual in-memory layout might include gaps between variables due to padding
  • Regarding the stack allocation:
    • Size must be known at compile time
    • Stack gets cleaned up when the variable goes out of scope

Heap is a dynamic memory region where you can request space at runtime. Instead of storing data directly you get a pointer to the allocated memory.

fn main() { let v = vec![65, 54, 57]; let s = String::from("slop"); }
Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ s: String │ │ "slop" (4B) │ ← 0x5000 │ ptr: 0x5000 ───┼───────┤ │ │ len: 5 │ └─────────────────┘ │ capacity: 5 │ ├─────────────────┤ ┌────────────────────┐ │ v: Vec<i32> │ │ 65 │ 54 │ 57 │ │ ← 0x4000 │ ptr: 0x4000 ───┼───────┤ │ │ │ │ │ len: 3 │ └────┴────┴────┴─────┘ │ capacity: 3 │ └─────────────────┘

So here notice one thing, that the stack has a pointer “ptr” that points to the actual data on the heap.

Lets take an example:

let x = String::from("x"); let y = x;

Before the let y=x; here is how the memory layout would look like:

Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ x: String │ │ "x" (1B) │ │ ptr: 0x1000 ───┼───────┤ │ │ len: 1 │ └─────────────────┘ │ capacity: 1 │ └─────────────────┘

After let y = x; what happens is x is moved to y so:

Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ x: (invalid) │ │ "x" (1B) │ ├─────────────────┤ │ │ │ y: String │ │ │ │ ptr: 0x1000 ───┼───────┤ │ │ len: 1 │ └─────────────────┘ │ capacity: 1 │ └─────────────────┘

Why is x invalid!?!?

Rust compiler invalidates x to prevent:

  • Double-free: Both x and y trying to free the same memory
  • Use-after-free: Using x after y has freed the memory

Lets take one more example (here y will borrow x):

let x = String::from("tpot"); let y = &x;
Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ x: String │ │ "tpot" (4B) │ ← 0x5000 │ ptr: 0x5000 ───┼───────┤ │ │ len: 4 │ └─────────────────┘ │ capacity: 4 │ ├─────────────────┤ │ y: &String │ │ ptr: &x │ └─────────────────┘

What are references?

  • They don’t own the data
  • They won’t outlive the data they refer to

Box is a smart pointer that lets you put a value on the heap explicitly

Stack Heap ┌─────────────────┐ ┌─────────────────┐ │ b: Box<i32> │ │ 5 (4 bytes) │ ← 0x6000 │ ptr: 0x6000 ───┼───────┤ │ └─────────────────┘ └─────────────────┘

Some types implement the Copy trait and are duplicated instead of moving them

let x = 100; let y = x; // x & y both a valid cause y is an independent copy

Types that implement Copy (there might be more so crosscheck plz):

  • integer types
  • floating point types
  • bool
  • char
  • Tuples of Copy types
  • Arrays of Copy types

You noticed one thing!? Only those data types that are allocated on the Stack implement Copy trait. This is what I noticed, might be true/false.

But Rust is memory safe right!? Yes, Rust prevents user after free and double-free but we can still create reference cycles with Rc<T> and RefCell<T>

struct Node { next: Option<Rc<RefCell<Node>>>, }

In the above code, it creates a cycle that won’t be freed.When two Rc pointers reference each other in a cycle, their reference counts never reach zero which means the memory is never freed. Why is that!? Cause Rc uses reference counting and to break cycles it uses Weak<T> for uni directional relationship which does not prevent deallocation when the strong reference is dropped.

  • Stack: Fast, fixed size, automatic cleanup, Last In First Out (LIFO)
  • Heap: Flexible size, requires allocation/deallocation, slower
  • Ownership: Each value has exactly one owner
  • Moving: Transfers ownership, prevents double-free
  • Borrowing: Temporary access without giving the ownership
Read Entire Article