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

  1. Each value has exactly one owner
  2. When the owner goes out of scope, the value is dropped
  3. 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 order

Early 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 Drop trait 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

šŸ¦€ Rust Programming Tutorial

Learn from Zero to Advanced

Built with Next.js and Tailwind CSS • Open Source