What does Rust have instead of a garbage collector?

I understand Rust doesn't have a garbage collector and am wondering how memory is freed up when a binding goes out of scope.

So in this example, I understand that Rust reclaims the memory allocated to 'a' when it goes out of scope.

{
    let a = 4
}

The problem I am having with this, is firstly how this happens, and secondly isn't this a sort of garbage collection? How does it differ from 'typical' garbage collection?


Solution 1:

Garbage collection is typically used periodically or on demand, like if the heap is close to full or above some threshold. It then looks for unused variables and frees their memory, depending on the algorithm.

Rust would know when the variable gets out of scope or its lifetime ends at compile time and thus insert the corresponding LLVM/assembly instructions to free the memory.

Rust also allows some kind of garbage collection, like atomic reference counting though.

Solution 2:

The basic idea of managing resources (including memory) in a program, whatever the strategy, is that the resources tied to unreachable "objects" can be reclaimed. Beyond memory, those resources can be mutex locks, file handles, sockets, database connections...

Languages with a garbage collector periodically scan the memory (one way or another) to find unused objects, release the resources associated with them, and finally release the memory used by those objects.

Rust does not have a GC, how does it manage?

Rust has ownership. Using an affine type system, it tracks which variable is still holding onto an object and, when such a variable goes out of scope, calls its destructor. You can see the affine type system in effect pretty easily:

fn main() {
    let s: String = "Hello, World!".into();
    let t = s;
    println!("{}", s);
}

Yields:

<anon>:4:24: 4:25 error: use of moved value: `s` [E0382]
<anon>:4         println!("{}", s);

<anon>:3:13: 3:14 note: `s` moved here because it has type `collections::string::String`, which is moved by default
<anon>:3         let t = s;
                     ^

which perfectly illustrates that at any point in time, at the language level, the ownership is tracked.

This ownership works recursively: if you have a Vec<String> (i.e., a dynamic array of strings), then each String is owned by the Vec which itself is owned by a variable or another object, etc... thus, when a variable goes out of scope, it recursively frees up all resources it held, even indirectly. In the case of the Vec<String> this means:

  1. Releasing the memory buffer associated to each String
  2. Releasing the memory buffer associated to the Vec itself

Thus, thanks to the ownership tracking, the lifetime of ALL the program objects is strictly tied to one (or several) function variables, which will ultimately go out of scope (when the block they belong to ends).

Note: this is a bit optimistic, using reference counting (Rc or Arc) it is possible to form cycles of references and thus cause memory leaks, in which case the resources tied to the cycle might never be released.