Intuition
Clearing out a shared fridge. You can’t ask every item whether it’s still wanted, so you start from the shopping lists stuck on the door (the roots). Everything a list mentions is kept, and so is anything those items mention (“the sauce for the pasta”). Whatever nobody’s list leads to goes in the bin, even two leftovers labelled as belonging to each other.
How it works
An object is live if the program could still use it: there is a chain of references to it from a root. Everything else is garbage, because no code can ever reach it again.
Mark and sweep finds the garbage in two passes:
- Mark. Push the objects the roots refer to onto a stack. Pop one; if it isn’t marked yet, mark it and push everything it refers to. Repeat until the stack is empty. Every marked object is reachable.
- Sweep. Walk the whole heap. Free each unmarked object and clear the marks on the rest, ready for next time.
Marking costs time in proportion to the live objects; sweeping, to the size of the heap.
Reference counting
The other classic approach keeps a count on each object of how many references point at it, and frees the object the moment the count drops to zero. Memory comes back immediately and predictably, but a cycle is never freed: in the figure, E and F each have a count of 1 (from each other) even though the program can’t reach either. Systems that use reference counting either add a cycle detector (Python), or ask the programmer to break cycles with weak references (Swift, Rust’s Rc).
In each language
| Language | How memory is reclaimed |
|---|---|
| Python | Reference counting frees most objects at once; a generational cycle collector finds unreachable cycles. gc.collect() runs it on demand. |
| JavaScript | Tracing collectors. V8 splits the heap into a young generation, collected often and quickly, and an old one, marked mostly in parallel with your code. |
| Go | A concurrent mark-and-sweep collector that runs alongside your goroutines with short pauses. GOGC trades memory for collector CPU time. |
| Java | Several tracing collectors to choose from; G1 is the default since Java 9, and ZGC aims for pauses under a millisecond. |
| Rust | No garbage collector. Each value has one owner and is freed when the owner goes out of scope. Rc and Arc add reference counting where sharing is needed, and can leak cycles. |
| C | Manual: malloc and free. Forgetting free leaks; freeing twice, or using memory after freeing it, is undefined behaviour. |
The costs
- Pauses. Some collectors stop your program while they mark. Modern ones do most work concurrently, but latency-sensitive code still notices.
- Memory headroom. A tracing collector works best with spare memory: a heap often runs at 1.5 to 2 times the live data.
- No timing guarantee. You can’t know exactly when an object is freed, so don’t rely on garbage collection to close files or sockets. Use
with(Python),try/finally,defer(Go) orDrop(Rust). - Leaks still happen. A cache or global list that keeps references holds objects alive forever: they are reachable, so they aren’t garbage.
Check yourself
Check yourself
Object G refers to a live object H, but nothing refers to G. Is G freed?
Reachability only follows arrows away from the roots. G pointing at H keeps H’s reference alive, not G.
Check yourself
Two objects refer only to each other. Which approach frees them on its own?
Each still has a reference count of 1, so plain reference counting keeps them. Mark and sweep never reaches them from a root, so it frees them.
Sources
- Richard Jones, Antony Hosking and Eliot Moss, The Garbage Collection Handbook, 2nd ed., chapters 2 (mark-sweep) and 5 (reference counting).
- Python documentation,
gcmodule. - The Go blog, “Getting to Go: The Journey of Go’s Garbage Collector”.
- V8 blog, “Trash talk: the Orinoco garbage collector”.
Finished this page?
Your list