How Rust manages memory without garbage collection through exclusive ownership, transfer semantics, and deterministic drops.
fn main() {
let s1 = String::from("Ferris");
// Ownership of the heap buffer is transferred to s2.
let s2 = s1;
// Error: use of moved value 's1' (E0382)
// println!("{}", s1);
println!("s2 holds the buffer: {}", s2);
} // s2 is dropped here; heap memory is freed immediately.Rust achieves memory safety without a garbage collector through a strict set of compile-time rules called Ownership.
Variables allocated on the stack (like integers, floats, booleans, and fixed arrays) implement the `Copy` trait. When assigned or passed into functions, their bits are duplicated on the stack with negligible cost.
Heap-allocated types (like `String`, `Vec<T>`, and `Box<T>`) are represented on the stack as a 'Fat Pointer' consisting of: (1) pointer address, (2) capacity, and (3) length.
When `s1` is assigned to `s2`, Rust simply copies the 24-byte stack descriptor from `s1` to `s2` and invalidates `s1`. This avoids expensive deep heap copies while preventing double-free vulnerabilities when variables fall out of scope.
The stack stores local variables with known, fixed sizes at compile time. The heap stores dynamically sized or growable data. In a `String`, the stack holds pointer (8B), length (8B), and capacity (8B) on a 64-bit architecture.
// Stack frame (24 bytes)
// [ ptr: 0x7fff0010 | cap: 8 | len: 6 ]
// │
// ▼
// Heap storage: [ 'F', 'e', 'r', 'r', 'i', 's' ]The Rust compiler tracks whether a variable must be dropped using static analysis or single-bit "drop flags" on the stack frame when branches conditionally move ownership.
fn conditional_move(condition: bool) {
let s = String::from("allocated");
if condition {
consume(s); // Moved here conditionally
}
// Compiler injects: if (!s_dropped) { drop(s); }
}