[Avg. reading time: 15 minutes]
Stack - Heap
Rust guarantees memory safety without a garbage collector by enforcing rules about ownership, borrowing, and lifetimes at compile time.
Most Rust programs use these memory regions:
- Static (Data) memory: values that exist for the entire program run
- Stack: per-function call frames, fast, fixed-size data
- Heap: dynamic, resizable data allocated at runtime
Static/Data Memory
This region holds data that is available for the entire lifetime of the program.
Typical residents:
- Program code (binary / text segment)
- Static variables (static)
- String literals (read-only bytes baked into the binary)
Example
fn main() { println!("Hello"); }
The bytes for “Hello” live in static memory. The program just references them.
A typical 64bit Windows
High Address
|---------------------|
| Stack | ~1 MB default (for each thread)
|---------------------|
| |
| Heap | grows dynamically
| |
|---------------------|
| Static / Data | fixed at compile time
|---------------------|
| Program Code |
|---------------------|
Low Address
Stack
Stack is LIFO (Last In First Out).
The stack stores stack frames created when functions are called. Each frame typically contains:
- Function arguments
- Local variables
- Bookkeeping info (return address, saved registers)
Stack data is usually:
- Known size at compile time
- Fast to allocate and free
- Automatically cleaned up when the function returns
Mac/Linux
8 MB default stack space.
ulimit -s
- Each process may have many threads. Ex: Google Chrome uses several threads.
- Each thread has its own stack.
- Stack memory is reserved not used immediately.
- Heap and Static are shared per process.
Process 1 (Chrome)
├── Thread 1 → Stack 1
├── Thread 2 → Stack 2
├── Thread 3 → Stack 3
├── Shared Heap 1
└── Shared Static/Data 1
Process 2 (VSCode)
├── Thread 1 → Stack 1
├── Thread 2 → Stack 2
├── Thread 3 → Stack 3
├── Shared Heap 2
└── Shared Static/Data 2
fn foo() { let y = 5; let z = 100; } fn main() { let x = 42; foo(); }
| Address | Name | Value |
|---|---|---|
| 0 | x | 42 |
After calling the function foo()
| Address | Name | Value |
|---|---|---|
| 2 | z | 100 |
| 1 | y | 5 |
| 0 | x | 42 |
Note: Memory address is taking into account DATA TYPE SIZE. It’s just a representation.
After foo() gets executed, control transfers to main, and the values are deallocated automatically.
| Address | Name | Value |
|---|---|---|
| 0 | x | 42 |
stateDiagram-v2
[*] --> Main
state Main {
[*] --> PushMainFrame
PushMainFrame: Push frame for main()
PushMainFrame --> AllocateX: x = 42
AllocateX --> CallFoo: Call foo()
CallFoo --> PushFooFrame: Push frame for foo()
state PushFooFrame {
[*] --> AllocateY: y = 5
AllocateY --> AllocateZ: z = 100
}
PushFooFrame --> PopFooFrame: Pop frame for foo()
PopFooFrame --> ReturnFoo: foo() returns
ReturnFoo --> PopMainFrame: Pop frame for main()
PopMainFrame --> [*]
}
Main --> [*]
Copy Trait
Types like i32, bool, char, and many small fixed-size structs implement Copy. Assignment duplicates bits.
fn main() { // i32 is a simple type and are normally stored on the stack. // copy trait let x = 42; let y = x; // The value bound to x is Copy, so no error will be raised. println!("{:?}", (x, y)); // The value bound to x is Copy, so no error will be raised. println!("{:p},{:p}", &x, &y); }
Heap
The heap stores data that is:
- Allocated at runtime
- Often resizable or variable-sized
- Accessed via a pointer stored on the stack
Common heap-backed types:
- String
- Vec
- Box
- HashMap<K, V>
fn main(){ let s1=String::from("hello"); println!("{}",s1) }

fn main() { let s = String::from("rust"); println!("Value : {}", s); println!("Length (bytes): {}", s.len()); println!("Capacity : {}", s.capacity()); println!("Stack addr : {:p}", &s); println!("Heap ptr : {:p}", s.as_ptr()); }
// English and international characters fn main() { let s = String::from("rust-ரஸ்ட்"); println!("Value : {}", s); println!("Length (bytes): {}", s.len()); println!("Capacity : {}", s.capacity()); println!("Stack addr : {:p}", &s); println!("Heap ptr : {:p}", s.as_ptr()); }
Move Trait
Heap-backed values are usually moved, not copied, because duplicating them would require duplicating heap allocations.
// Move Trait (Heap) fn main() { let mut name = String::from("Hello World"); println!("Memory address of name: {},{:p} \n", name,&name); //moving let name1 = name; println!("Memory address of name1: {},{:p} \n", name1,&name1); //println!("Memory address of name: {},{:p} \n", name,&name); // Setting up another Value for the variable name name = String::from("Dear World"); println!("Memory address of name: {},{:p}\n", name,&name); }
Example: pdf (stack) - printed book (heap)
// Copy Trait (Stack - because of using String Literal) fn main() { let name = "Hello World"; println!("Memory address of name: {},{:p} \n", name,&name); //Copying let name1 = name; println!("Memory address of name1: {},{:p} \n", name1,&name1); println!("Memory address of name: {},{:p} \n", name,&name); }
1: adafruit.com
