Guide

Once a program has allocated memory, something must free it. Languages make four broad choices. The choice decides who has to think about freeing, and what goes wrong when they get it wrong.

Concept

Before you start: a use-after-free bug means reading memory that has already been freed. A double free means freeing the same memory twice. Both can corrupt data or open security holes.

The four approaches

ApproachLanguagesHow memory is freedMain costs and risks
ManualC, C++ (raw new and delete)The programmer calls freeUse-after-free, double free, leaks; the most serious security bugs in C and C++ code
Ownership and borrowingRust, C++ RAII and smart pointersThe compiler inserts the free when the owner goes out of scopeLearning curve; graphs and doubly linked lists are awkward
Reference countingSwift, Objective-C (ARC), CPython, Rust Rc and ArcEach object counts its references and is freed at zeroA count update on every copy; cycles leak unless something else collects them
Tracing GCGo, Java, C#, JavaScript, RubyA collector periodically finds unreachable objectsExtra memory headroom, CPU for collection, pauses

Manual management

Manual freeing is fast and predictable. It is also the source of a large share of serious bugs. Microsoft and Google have both reported that roughly 70% of serious security vulnerabilities in their large C and C++ codebases are memory-safety issues. That is why there is a strong push toward memory-safe languages.

Ownership and borrowing

Rust gives each value exactly one owner. When the owner goes out of scope, the compiler inserts the free. You can lend out borrows, which are references. The borrow checker proves at compile time that no reference outlives its data, and that nothing is changed while someone else reads it.

The result is C-like speed and predictability with no runtime collector. The cost is that you fight the compiler while you learn it.

Reference counting

Reference counting frees an object the moment its last reference goes away. Memory use stays tight, and there are no collection pauses. CPython uses it for every object, which is why cleanup in Python feels deterministic.

The weakness is cycles. If A points to B and B points to A, both counts stay at 1 forever. CPython adds a backup cycle collector for this. Swift asks you to mark one side weak. Freeing a huge object graph can also cause a pause, because a cascade of counts reach zero at once.

Tracing garbage collection

A tracing collector ignores counts. It periodically starts from the roots, finds everything reachable, and treats the rest as free. That handles cycles naturally, and it keeps pointer writes cheap for your program.

The price is memory headroom and collection work. Those costs create the central trade-off of the next pages, covered in how tracing collectors work.

Walking through it: which approach fits

Consider three services with the same bug rate target:

  • A latency-critical proxy that must never pause: ownership (Rust) or manual (C) fits, because nothing runs in the background.
  • A typical web backend with many short requests: tracing GC (Go, Java) fits, because most objects die young and the team gets safety without thinking about frees.
  • A Python data script with many short-lived objects: reference counting works, and the rare cycles are collected by the backup collector.

The approach follows from what the team can afford to think about, not from which one is "best".

Common mistakes

  • Assuming reference counting never leaks. Cycles keep counts above zero.
  • Assuming tracing GC prevents all leaks. A reachable object is never collected, even if you will never use it.
  • Mixing approaches carelessly. Foreign-function calls between C and a managed language need explicit ownership rules.

Check yourself

  • Why can two Python objects that point at each other outlive all outside references? Each count stays at one because the other object holds a reference. The backup cycle collector is needed to free them.
  • Why does a tracing GC avoid the cycle problem? It starts from the roots and asks what is reachable. Two objects that only reference each other are unreachable, so both are freed.

Key takeaways

  • Four approaches: manual, ownership, reference counting, tracing. Each trades safety, predictability and overhead.
  • Manual freeing causes most serious memory-safety bugs in C and C++ code.
  • Reference counting frees immediately but needs cycle handling. Tracing handles cycles but pays in pauses and headroom.
  • Ownership gives predictability and safety with no collector, at the cost of learning the compiler's rules.

Further learning

Next, see how a tracing collector finds garbage and keeps pauses short, in how tracing garbage collectors work.

A Guide to the Go Garbage Collector explains the trade-offs of a tracing collector in one language.