Garbage Collection Fundamentals
Garbage collection through reachability tracing, generations, and pause trade-offs.
Garbage collection is automatic memory reclamation. Instead of asking the programmer to free each object manually, the runtime decides when an object is no longer reachable and reuses its memory. That sounds simple, but the hard part is deciding what counts as no longer in use without stopping the program for too long.
Most modern collectors are tracing collectors. They begin from a set of roots, such as stack variables, CPU registers, global variables, and references held by native runtime structures. Starting from those roots, the collector follows references through the heap and marks every reachable object. Anything left unmarked is considered garbage because the program has no path to it anymore.
The simplest tracing design is mark and sweep. In the mark phase, the collector discovers all reachable objects. In the sweep phase, it walks the heap and returns unmarked regions to a free list. This works, but it can fragment memory because live objects remain scattered. That is why many runtimes also compact memory by moving live objects together and updating references. Compaction improves allocation speed and cache locality, but moving objects makes the collector more complex.
A common optimisation is generational collection. It is based on an empirical rule: most objects die young. Instead of scanning the whole heap every time, the runtime keeps newly allocated objects in a young generation and collects that area frequently. Objects that survive enough collections are promoted to an old generation that is scanned less often. This reduces total work, but it requires write barriers so the runtime can track references from old objects back into the young generation.
Pause time is the main operational constraint. A fully stop-the-world collector pauses application threads while it marks or compacts memory. That is easier to implement, but bad for interactive systems and latency-sensitive services. Many modern runtimes therefore perform part of the work concurrently with the program. Go uses a concurrent mark and sweep collector. Java offers several collectors with different latency and throughput goals, such as G1, ZGC, and Shenandoah in current ecosystems. The tradeoff is predictable: less pause time usually means more runtime overhead, more metadata, or more background CPU use.
Python is a useful contrast because CPython relies heavily on reference counting. Each object tracks how many references point to it. When that count falls to zero, the object can be reclaimed immediately. This makes destruction prompt, but it cannot break reference cycles on its own. If object A points to B and B points back to A, neither count reaches zero even if the rest of the program has forgotten them. Python therefore adds a cyclic collector to detect and clean up those unreachable cycles.
Garbage collection is not free memory creation. If a program keeps references accidentally, the collector will preserve that memory forever because it still looks live. Long-lived caches, global maps, event listeners, and large object graphs are common sources of memory growth. Finalisers can also complicate reclamation because the runtime may need extra steps before releasing an object.
In practice, garbage collection is a trade between programmer effort and runtime cost. Manual memory management can be lean and predictable but easy to get wrong. Garbage collection reduces use-after-free bugs and double frees, yet it introduces pauses, metadata overhead, and the need for careful heap tuning in some systems. The key idea is simple: reclaim what the program can no longer reach. The engineering challenge is doing that fast enough, safely enough, and with small enough pauses for the workload in front of you.