Home / Software y Cloud / The memory janitor: how the garbage collector decides what to collect

The memory janitor: how the garbage collector decides what to collect

Ilustración del recolector de basura de memoria

Your program creates objects endlessly: a list here, a string there, a temporary user. Each one occupies a corner of memory. And when you stop using it, it would stay there forever, piling up until the process suffocates. The garbage collector (GC) takes care of that trash: it is the language-runtime component that reclaims memory no one needs anymore.

A C or C++ programmer must free memory by hand (with free() or delete), and a mistake either leaks memory or releases something still in use. In managed languages such as Java, Go, Python, JavaScript or Kotlin, that work falls to the GC. The technical question is: how does the collector know which memory it can safely reclaim?

Reachability: what can be reached lives, what cannot dies

The core idea of a tracing GC is reachability. The collector starts from the roots: local variables on the stack, registers, global and static variables. Any object you can reach by following references from a root is reachable and is kept. Everything else can be reclaimed. It does not matter whether a cycle of references exists (A points to B and B to A): if no root can reach them, both are collected.

Mark-and-sweep: marking to know what stays alive

The classic algorithm is mark-and-sweep. During mark, the collector starts from the roots and visits every reachable object, flagging it as alive. During sweep, it walks the whole heap and frees the objects that never got flagged. The mark bits can live in a separate bitmap, away from the data, so the object is not polluted with metadata.

Tri-color marking: the state that prevents reclaiming live objects

Behind the classic mark hides a refinement called tri-color marking. Each object is conceived with one of three colors: white (unvisited, a candidate to be reclaimed), grey (visited but with references still pending) and black (visited and fully processed). The algorithm demands an invariant: no black object may point to a white object. If the collector ran concurrently with the program (the mutator) and that invariant broke, a white object still in use could be reclaimed by mistake — the so-called lost object problem.

Write barriers and pauses: the price of concurrency

For the GC to run at the same time as the program without stopping it, we use write barriers: small routines that intercept every pointer modification. Thanks to them, when a black object gains a reference to a white one, that white object is re-marked grey and the invariant is restored. The cost is a few extra instructions per assignment, but in exchange the GC can collect memory concurrently or incrementally, shrinking the dreaded stop-the-world pauses.

Generational GC: collecting what dies young

Most objects are ephemeral: they are born and fall out of use within milliseconds. Hence the generational hypothesis. The heap is split into generations (typically young and old). The GC runs often on the sparsely populated young generation, which is cheap and fast; only from time to time does it promote surviving objects to the old generation, which is collected less frequently. To avoid scanning the whole old generation looking for references to the young one, it keeps a remembered set: the set of old-generation objects that point into the new generation and must be treated as extra roots.

And the one that counts references

There is another family of collectors: reference counting. Each object holds a counter of how many references point to it; when it reaches zero, it is freed immediately. That is the strategy of Python and Swift, and it is attractive: memory is reclaimed immediately, with no long global pauses. Its Achilles heel is the cycle: two objects that reference each other never hit a zero counter. That is why Python pairs counting with a generational GC that breaks cycles, and Swift requires the programmer to declare weak references.

Not everything is elegant: wasted bytes and grains of sand

No approach is free. The GC consumes extra CPU and memory, and managed languages usually carry a higher baseline cost than their compiled cousins. Choosing when and how to collect is, in systems like the modern JVM or the Go runtime, a fine compromise between latency, throughput and memory footprint. But the alternative — asking a human to remember to free every object — is the perfect recipe for leaks and dangling accesses.

At heart, the garbage collector is not magic: it is a janitor with a bitmap, three colors and a measured pause, who knows exactly which objects it should leave you alone with.