July 31, 2025•3 min
How does Rust ensure memory safety without a garbage collector?
m
mayoTable of Contents
Rust guarantees memory safety at compile time using three key mechanisms: ownership, borrowing, and lifetimes. These ensure no memory leaks, data races, or dangling pointers without the need for a garbage collector.
The C/C++ Problem
C and C++ give developers complete control over memory, but this leads to critical safety issues:
Dangling Pointers:
char* get_string() {
char buffer[100] = "hello"; // Stack allocated
return buffer; // Returns pointer to freed memory
} // ERROR: buffer is destroyed here
int* ptr = malloc(sizeof(int));
free(ptr);
*ptr = 42; // ERROR: Use after free
Memory Leaks:
void leak_memory() {
int* data = new int[1000]; // Heap allocation
if (some_condition) {
return; // ERROR: Memory never freed
}
delete[] data; // Only freed on normal path
}
Double Free:
int* ptr = malloc(sizeof(int));
free(ptr);
free(ptr); // ERROR: Double free causes undefined behavior
Java's Garbage Collection Approach
Java solves these issues with automatic memory management:
✅ Pros:
- No dangling pointers (references become null when objects are collected)
- No memory leaks for reachable objects
- No double free errors
❌ Cons:
- Runtime overhead: GC pauses can cause unpredictable latency
- Memory overhead: Additional metadata for tracking objects
- No deterministic cleanup: Objects freed at GC's discretion, not immediately
// Java - memory managed automatically
String createString() {
String s = new String("hello"); // Heap allocated
return s; // Safe: GC will clean up when no longer referenced
} // No explicit cleanup needed
1. Ownership Rules
- Each value in Rust has a single owner.
- When the owner goes out of scope, the value is dropped (memory freed).
- Prevents double frees and memory leaks.
Example:
fn main() {
let s = String::from("hello"); // `s` owns the string
takes_ownership(s); // Ownership moved → `s` is invalid here
// println!("{}", s); // ERROR: borrow of moved value
}
fn takes_ownership(s: String) {
println!("{}", s);
} // `s` is dropped here
2. Borrowing & References
- Allows immutable (
&T
) or mutable (&mut T
) borrows. - Enforced rules:
- Either one mutable reference or multiple immutable references (no data races).
- References must always be valid (no dangling pointers).
Example:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // OK: Immutable borrow
let r2 = &s; // OK: Another immutable borrow
// let r3 = &mut s; // ERROR: Cannot borrow as mutable while borrowed as immutable
println!("{}, {}", r1, r2);
}
3. Lifetimes
- Ensures references never outlive the data they point to.
- Prevents dangling references.
Example:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("hello");
let result;
{
let s2 = String::from("world");
result = longest(&s1, &s2); // ERROR: `s2` doesn't live long enough
}
// println!("{}", result); // `result` would be invalid here
}
Why No Garbage Collector (GC)?
- Zero-cost abstractions: No runtime overhead.
- Predictable performance: Memory is freed deterministically.
- No runtime pauses: Unlike GC-based languages (Java, Go).
Key Takeaways
✅ Ownership: Prevents memory leaks.
✅ Borrowing: Prevents data races.
✅ Lifetimes: Prevents dangling pointers.
Rust's model ensures memory safety without runtime checks, making it both safe and fast.
Back to Blog
Share: