Understanding Rust Memory Management: Allocation and Automatic Deallocation

Memory management in Rust revolves around two fundamental operations: allocation and deallocation. Unlike languages that rely on garbage collectors or manual memory management, Rust employs a distinct strategy integrated directly into its ownership system.

Allocation

When creating variables in Rust, the compiler determines whether to store data on the stack or heap based on whether the size is known at compile time.

Stack Allocation: Fixed-size values with known dimensions are stored directly on the stack. These include primitive types like integers, booleans, and characters. The stack provides fast allocation since it simply involves moving the stack pointer.

Heap Allocation: Values whose size cannot be determined at compile time—such as strings, vectors, and other dynamic collections—reside on the heap. When creating such values, Rust allocates memory on the heap and stores a pointer to that memory location on the stack.

fn main() {
    let fixed_value = 42; // stored on stack
    let dynamic_string = String::from("example"); // heap allocation with pointer on stack
}

Varialbe Scope and Automatic Deallocation

The lifetime of a variable in Rust is tied to its enclosing scope. When execution exits the block where a variable is defined, that variable goes out of scope and its memory is automatically reclaimed.

fn main() {
    let message = " greeting ";
    
    {
        let temp = "temporary";
        println!("{temp}");
    } // temp is automatically dropped here
    
    println!("{message}");
    // println!("{temp}"); // this would fail - temp is out of scope
}

The compiler inserts a call to the drop function automatically at the end of a variable's scope. This function contains the logic necessary to free the allocated memory—this is particularly important for heap-allocated types.

Ownership: Move vs Copy Semantics

When assigning variables to new bindings, Rust handles stack and heap data differently.

Stack-Allocated Types: Copy Semantic

Types stored entirely on the stack implement the Copy trait by default. When assigned, the data is duplicated rather than transferred.

fn main() {
    let num1 = 100;
    let num2 = num1; // data is copied to num2
    
    println!("num1: {}", num1); // valid - num1 still accessible
    println!("num2: {}", num2);
}

Heap-Allocated Types: Move Semantic

Heap-allocated types use move semantics by default. Ownership transfers to the new binding, and the original varible becomes invalid.

fn main() {
    let resource1 = String::from("owned");
    let resource2 = resource1; // ownership transfers to resource2
    
    println!("resource2: {}", resource2);
    // println!("{}", resource1); // compile error - resource1 is moved
}

Attempting to use the original variable after a move results in a compilation error, preventing dangling pointers and double-free bugs.

Memory Layout

When a heap-allocated object like String is created, the memory structure looks like this:

Stack (variable)          Heap (data)
-----------------        ----------------
| ptr | address | ----> | index | byte    |
-----------------        ----------------
| len | 5       |       | 0     | 'o'     |
-----------------        ----------------
| cap | 5       |       | 1     | 'w'     |
-----------------        ----------------
                        | 2     | 'n'     |
                        ----------------
                        | 3     | 'e'     |
                        ----------------
                        | 4     | 'd'     |
                        ----------------

Move operations transfer the pointer, length, and capacity to the new variable without duplicating heap data. The source variable is invalidated, ensuring only one valid reference exists at any time.

Clone: Deep Copy Operation

To explicitly duplicate heap data when needed, the clone method perfroms a deep copy.

fn main() {
    let original = String::from("duplicate");
    let copied = original.clone();
    
    println!("original: {}", original); // both remain valid
    println!("copied: {}", copied);
}

Calling clone incurs the performance cost of allocating new memory and copying all data.

Types Implementing Copy

The following types automatically implement Copy:

  • Integer types (i32, u64, etc.)
  • Boolean type (bool)
  • Floating-point types (f64, f32)
  • Character type (char)
  • Tuples containing only Copy types (e.g., (i32, i32))

Types that implement Drop cannot implement Copy, as this would create ambiguity about when to free memory.

Tags: rust memory-management ownership allocation deallocation

Posted on Tue, 07 Jul 2026 17:05:14 +0000 by scotthoff