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?
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.
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:
Releasing the memory buffer associated to each String 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.
With a language where you must manually manage memory, the distinction between the stack and the heap becomes critical. Every time you call a function, enough space is allocated on the stack for all variables contained within the scope of that function. When the function returns, the stack frame associated with that function is "popped" off the stack, and the memory is freed for future use.
From a practical standpoint, this inadvertent memory cleaning is used as a means of automatic memory storage that will be cleared at the end of the function's scope.
There is more information available here: https://doc.rust-lang.org/book/the-stack-and-the-heap.html
Some languages have reference counting, some have garbage collectors. Rust avoids both, instead, it allows only a single variable name or alias if you like to own a memory location at any point in time. You can move the ownership from one variable name to another, but you can’t have two variable names pointing to the same memory address (Except for shared Ownership. Rust provides the reference-counted pointer types Rc and Arc. Because sometimes it’s difficult to find every value a single owner that has the lifetime you need).
Rust uses a relatively unique memory management approach that incorporates the idea of memory “ownership”. Basically, Rust keeps track of who can read and write to memory. It knows when the program is using memory and immediately frees the memory once it is no longer needed. It enforces memory rules at compile time, making it virtually impossible to have runtime memory bugs. You do not need to manually keep track of memory. The compiler takes care of it.
Discord recently switched from Go to Rust in one of its services just because garbage collector was causing latency. They explained very well why they did this and you will learn more about the garbage collector and rust memory system:
Rc
/Arc
do.
Success story sharing
new()
function like C, they are just static functions, and in particular something likelet x = MyStruct::new()
creates its object on the stack. The real indicator of heap allocation isBox::new()
(or any of the structures that depend on Box).