Understanding Rust’s Memory Model
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.
Before that WTF is Memory?
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)
The Stack: Fast and Organized
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
The Heap: Slower but Spacious
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.
Ownership & Memory
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
Borrowing & 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: Explicitly Heap Allocating
Box is a smart pointer that lets you put a value on the heap explicitly
let b = Box::new(5);
Stack Heap
┌─────────────────┐ ┌─────────────────┐
│ b: Box<i32> │ │ 5 (4 bytes) │ ← 0x6000
│ ptr: 0x6000 ───┼───────┤ │
└─────────────────┘ └─────────────────┘
Copy in Rust
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.
Memory Leaks in Rust
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.
TLDR ;)
- 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