Garbage collection.
A program creates objects on the heap. Some it stops using. The GC's job is to figure out which ones are no longer reachable from anywhere live, and reclaim their memory: without the program noticing. Watch tri-color mark-and-sweep walk a small object graph, handle a cycle, and free the unreachable bits.
Eight objects on the heap. Only one is rooted (User, A): meaning it's reachable from a stack variable, a global, or a register. The rest are connected to each other or floating. The GC's job: find everything reachable from roots; reclaim the rest. We'll walk a classic mark-and-sweep, then mention how generational and concurrent collectors improve on it.
- GC root
- A reference from outside the heap that's definitely live: stack locals, global variables, CPU registers, thread-local storage. Reachability starts here.
- Reachable
- Reachable from a root by following references transitively. Everything else is garbage.
- Tri-color marking
- White = unvisited, gray = visited but children unscanned, black = visited and children scanned. The mark phase moves objects from white → gray → black.
Why ref-counting alone isn't enough
Reference counting increments a counter on every assignment, decrements on every "I'm done." When the count hits 0, free immediately. Simple, predictable. Until you have a cycle: A points to B, B points to A. Neither count ever reaches 0, even when nothing else references either of them. Pure ref-counting languages (Swift, early Python) need cycle detection as a backup, or you have to manually break cycles with weak references.
Generational GC · the killer optimisation
The generational hypothesis: most objects die young. A web request creates short-lived objects; the response gets serialized and they all become garbage in milliseconds. Generational collectors split the heap into a small "young" generation and a larger "old" generation. They collect young aggressively (cheap, finds most garbage) and old rarely (expensive, but most stuff there is long-lived). JVM's G1 and ZGC, V8's scavenger + Major GC, .NET's tiered GC: all use this principle.
Concurrent vs stop-the-world
Old GCs paused the application entirely while collecting. Multi-second pauses on big heaps. Modern GCs (Go's GC, JVM's ZGC, V8's incremental marker) do most of the work concurrently with the application. They still need short stop-the-world phases for safety (e.g. marking roots), but those are measured in single-digit milliseconds even on 100 GB heaps. The catch: concurrent GCs need write barriers, every pointer write costs a few extra instructions to inform the GC. The cost is a few percent of CPU; the payoff is no multi-second pauses.
GC internals · the long version →
Go's tricolor concurrent GC, JVM's G1 / ZGC / Shenandoah, V8's scavenger + Major GC, .NET's tiered GC, write barriers in detail.
Open the Codex →