Skip to content
rusthacks.dev
Systems

Why Rust Has No Garbage Collector and What It Uses Instead

Rust frees memory at compile-time-determined points using ownership and Drop, not a runtime collector. See what that buys in predictability and footprint, what it costs in design effort, and where reference counting fills the gap.

3 min read
Summarize in AI
Flat illustration for the post “Why Rust Has No Garbage Collector and What It Uses Instead”, composed around ownership.

Garbage collection is the standard answer to "who frees this memory?" in managed languages: a runtime periodically finds what is no longer reachable and reclaims it. Rust gives a different answer, ownership, which is decided when the program is compiled. The effect is memory safety with no collector, and the consequences show up in latency, footprint and how you design data. This post explains the mechanism with a small program. It compiles on Rust 1.90 with edition 2024.

Deterministic cleanup

rust
struct Noisy(&'static str);

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

fn main() {
    let _a = Noisy("a");
    {
        let _b = Noisy("b");
        println!("inner scope");
    }
    let c = Noisy("c");
    drop(c);
    println!("end of main");
}
Text
inner scope
drop b
drop c
end of main
drop a

b is dropped at the closing brace of its scope, c at the explicit drop, and a at the end of main, in reverse order of declaration. The compiler decides these points; nothing is traced at run time. For a Vec<String> the drop frees each string and then the buffer, so a whole structure is released at one predictable moment (ownership explained).

What a collector costs, and what Rust avoids

ConcernTracing GCRust ownership
When memory is freedAt some later collectionAt scope exit, deterministic
PausesPossible, tuned by collector designNone from memory management
Runtime and metadataCollector, write barriers, headersNone; a plain allocator
Non-memory resourcesNeed finalizers or explicit closeSame mechanism: Drop closes files and unlocks mutexes
Programmer effortLow for ownership, higher for tuningHigher for ownership design, low for tuning

Modern collectors are sophisticated, and pause times in tuned systems can be very small; the claim is not that GC is bad. The claim is that a language without one can run in places where a collector cannot: kernels, microcontrollers, audio callbacks (real-time audio) and programs that embed into hosts written in other languages without a second runtime.

Drop is not only for memory

rust
use std::sync::Mutex;

fn main() {
    let m = Mutex::new(vec![1, 2, 3]);
    {
        let mut guard = m.lock().unwrap();
        guard.push(4);
    } // guard dropped here: lock released
    println!("{:?}", m.lock().unwrap());
}
Text
[1, 2, 3, 4]

The same rule that frees a buffer unlocks the mutex, closes a file and finishes a transaction. This pattern, called RAII, is why Rust code rarely has explicit close calls, and why forgetting to release a lock is hard to do by accident.

Shared ownership when you need it

rust
use std::rc::Rc;

fn main() {
    let shared = Rc::new(String::from("config"));
    let a = Rc::clone(&shared);
    println!("{} owners", Rc::strong_count(&shared));
    drop(a);
    println!("{} owner", Rc::strong_count(&shared));
}
Text
2 owners
1 owner

When several parts of a program must own a value, Rc (single-threaded) and Arc (thread-safe) count references and free the value when the count reaches zero. It is deliberate, visible in the type, and local: only values you wrap pay for it (Box, Rc and Arc compared). Unlike a tracing collector it cannot reclaim cycles, which is why Weak exists.

Patterns that replace "just use a GC"

  • Arenas: allocate many objects with one lifetime in a bumpalo or typed-arena region and free them all at once; ideal for compilers and parsers.
  • Indices into a Vec: graphs and trees as Vec<Node> with integer ids avoid pointers and lifetimes entirely (data-oriented design).
  • Cloning: sometimes the right answer is a copy, especially for small data.

Honest limits

The borrow checker cost is real: some designs that are natural with a collector (cyclic object graphs, observer patterns with back-references) need rethinking or Rc<RefCell<T>>. Memory can still leak in safe Rust, through cycles or mem::forget (memory leaks in safe code). And allocation itself, done by the system allocator, has costs and fragmentation you may need to tune (allocators compared). Rust removes the collector, not the need to think about memory.

Also inRust
Taggedmemory
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.