Advanced Level
Memory Management Deep Dive
Chapter 23: Memory Management Deep Dive š¾
Understand how Rust manages memory without a garbage collector through ownership, borrowing, and the stack/heap model.
Stack vs Heap
Stack Allocation
fn main() {
// These values are stored on the stack
let x = 5;
let y = "Hello";
let z = true;
// Stack values have fixed size known at compile time
println!("x: {}, y: {}, z: {}", x, y, z);
}Heap Allocation
fn main() {
// This value is stored on the heap
let s = String::from("Hello, world!");
// The stack contains a pointer to the heap data
println!("s: {}", s);
}Ownership Model
Ownership Rules
- Each value has exactly one owner
- When the owner goes out of scope, the value is dropped
- Ownership can be transferred (moved)
fn main() {
let s1 = String::from("Hello");
let s2 = s1; // s1 is moved to s2
// println!("{}", s1); // Error: s1 is no longer valid
println!("{}", s2); // This works
let x1 = 5;
let x2 = x1; // x1 is copied to x2 (both are valid)
println!("x1: {}, x2: {}", x1, x2);
}Move Semantics
fn main() {
let vec1 = vec![1, 2, 3];
let vec2 = vec1; // vec1 is moved to vec2
// println!("{:?}", vec1); // Error: vec1 is no longer valid
println!("{:?}", vec2);
let arr1 = [1, 2, 3];
let arr2 = arr1; // arr1 is copied to arr2 (both are valid)
println!("{:?}, {:?}", arr1, arr2);
}Borrowing and References
Immutable Borrowing
fn main() {
let s = String::from("Hello");
let r1 = &s; // First immutable reference
let r2 = &s; // Second immutable reference
println!("{} and {}", r1, r2);
// Both references can be used because they're immutable
}Mutable Borrowing
fn main() {
let mut s = String::from("Hello");
let r1 = &mut s; // Mutable reference
// let r2 = &mut s; // Error: cannot have multiple mutable references
// let r3 = &s; // Error: cannot have immutable reference while mutable exists
r1.push_str(", world!");
println!("{}", r1);
}Reference Lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string is long");
let string2 = String::from("xyz");
let result = longest(&string1, &string2);
println!("The longest string is: {}", result);
}The Drop Trait
Custom Drop Behavior
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created.");
}
// c and d are automatically dropped here in reverse orderEarly Drop with std::mem::drop
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
drop(c); // Explicitly drop c early
println!("CustomSmartPointer dropped before end of main.");
}Box for Heap Allocation
Allocating on the Heap
fn main() {
let b = Box::new(5);
println!("b = {}", b);
// Box allocates data on the heap but the pointer is on the stack
// When b goes out of scope, the heap data is automatically freed
}Recursive Data Structures
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
// Without Box, this would cause infinite size compilation error
}Rc for Shared Ownership
Reference Counting
use std::rc::Rc;
enum List {
Cons(i32, Rc<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}RefCell for Interior Mutability
Runtime Borrow Checking
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
use List::{Cons, Nil};
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));
// Uncommenting this would cause a panic due to reference cycle
// println!("a next item = {:?}", a.tail().unwrap().borrow());
}Practical Examples
Example 1: Memory Pool Implementation
use std::alloc::{alloc, dealloc, Layout};
use std::ptr;
struct MemoryPool {
data: *mut u8,
layout: Layout,
used: usize,
capacity: usize,
}
impl MemoryPool {
fn new(capacity: usize) -> MemoryPool {
let layout = Layout::from_size_align(capacity, 8).unwrap();
let data = unsafe { alloc(layout) };
if data.is_null() {
panic!("Failed to allocate memory pool");
}
MemoryPool {
data,
layout,
used: 0,
capacity,
}
}
fn allocate(&mut self, size: usize) -> Option<*mut u8> {
if self.used + size <= self.capacity {
let ptr = unsafe { self.data.add(self.used) };
self.used += size;
Some(ptr)
} else {
None
}
}
fn reset(&mut self) {
self.used = 0;
}
}
impl Drop for MemoryPool {
fn drop(&mut self) {
unsafe {
dealloc(self.data, self.layout);
}
}
}
fn main() {
let mut pool = MemoryPool::new(1024);
if let Some(ptr1) = pool.allocate(100) {
println!("Allocated 100 bytes at {:?}", ptr1);
}
if let Some(ptr2) = pool.allocate(200) {
println!("Allocated 200 bytes at {:?}", ptr2);
}
pool.reset();
println!("Pool reset, can allocate again");
if let Some(ptr3) = pool.allocate(50) {
println!("Allocated 50 bytes at {:?}", ptr3);
}
// Memory is automatically freed when pool goes out of scope
}Example 2: Custom Smart Pointer with Memory Tracking
use std::ops::Deref;
use std::ptr::drop_in_place;
static mut ALLOCATED_MEMORY: usize = 0;
struct TrackedBox<T> {
ptr: *mut T,
}
impl<T> TrackedBox<T> {
fn new(value: T) -> TrackedBox<T> {
unsafe {
ALLOCATED_MEMORY += std::mem::size_of::<T>();
}
let ptr = Box::into_raw(Box::new(value));
TrackedBox { ptr }
}
fn get_allocated_memory() -> usize {
unsafe { ALLOCATED_MEMORY }
}
}
impl<T> Deref for TrackedBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unsafe { &*self.ptr }
}
}
impl<T> Drop for TrackedBox<T> {
fn drop(&mut self) {
unsafe {
drop_in_place(self.ptr);
ALLOCATED_MEMORY -= std::mem::size_of::<T>();
}
}
}
fn main() {
let b1 = TrackedBox::new(42);
let b2 = TrackedBox::new(String::from("Hello, world!"));
println!("Allocated memory: {}", TrackedBox::get_allocated_memory());
println!("b1: {}", *b1);
println!("b2: {}", *b2);
// When b1 and b2 go out of scope, they're dropped and memory is freed
}Memory Layout and Alignment
Understanding Memory Alignment
use std::mem;
#[repr(C)]
struct AlignedStruct {
a: u8,
b: u32,
c: u16,
}
fn main() {
println!("Size of AlignedStruct: {}", mem::size_of::<AlignedStruct>());
println!("Alignment of AlignedStruct: {}", mem::align_of::<AlignedStruct>());
// Default Rust alignment might reorder fields for efficiency
println!("Size of (u8, u32, u16): {}", mem::size_of::<(u8, u32, u16)>());
}Common Mistakes
ā Double Free Errors
fn main() {
let x = String::from("Hello");
let y = x; // x is moved to y
// Rust prevents this at compile time
// drop(x); // Error: x is no longer valid
drop(y); // This works
}ā Dangling References
fn dangle() -> &String {
let s = String::from("Hello");
&s // Error: s will be dropped but we're returning a reference
}
fn no_dangle() -> String {
let s = String::from("Hello");
s // This works - we return the String directly
}ā Memory Leaks
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
parent: RefCell<Option<Rc<Node>>>,
}
// This can create reference cycles leading to memory leaks
// Use Weak references to break cycles:
use std::rc::Weak;
#[derive(Debug)]
struct BetterNode {
value: i32,
children: RefCell<Vec<Rc<BetterNode>>>,
parent: RefCell<Option<Weak<BetterNode>>>,
}Key Takeaways
- ā Rust manages memory through ownership without a garbage collector
- ā Stack allocation is fast and automatic; heap allocation requires explicit management
- ā Each value has exactly one owner, preventing double free errors
- ā Borrowing allows temporary access to values without transferring ownership
- ā
The
Droptrait enables custom cleanup when values go out of scope - ā
Box<T>provides heap allocation with automatic cleanup - ā
Rc<T>enables shared ownership with reference counting - ā
RefCell<T>provides interior mutability with runtime borrow checking - ā Follow Rust naming conventions and idioms for memory management
Ready for Chapter 24? ā Performance Optimization