Opening dozens of browser tabs without a thought for RAM, creating lists, dictionaries or objects and then forgetting about them: almost no programmer worries today about freeing memory manually. That work, invisible and enormous, is done by a component called a garbage collector (GC). This article explains how it works inside, which algorithms it uses, and why your computer keeps running despite thousands of silent errors.
The problem: memory does not drain by itself
When a program asks the operating system for memory (with a call such as malloc or a new), that block becomes occupied. In languages like C, the programmer must hand it back explicitly with free. If you forget, the program leaks memory (memory leak); if you free it twice or use it after freeing, you corrupt data or trigger segmentation faults.
Managed languages (Java, Python, Go, JavaScript, C#, Ruby) remove that risk with a GC: the runtime itself decides when an object is no longer used and reclaims its memory automatically. The key question is: how does the collector know that an object is no longer in use?
Reachability and roots: the core idea
Every GC rests on the concept of reachability. An object is “alive” if it can still be reached from a fixed starting point called the root set. The roots include the local variables of running threads, CPU registers, global variables and method parameters.
From those roots, the collector walks the network of objects following the references each one keeps to others. Anything not reachable from a root is considered dead and its memory is freed. This fundamental idea — tracing, or mark-and-sweep — is the basis of almost every modern GC.
Mark & sweep
The classic algorithm has two phases. In the first, mark, the collector starts from the roots and walks the object graph, marking every live object. In the second, sweep, it walks the heap (the dynamic memory area) and returns to the system all the memory that was not marked.
The cost of this sweep phase is proportional to the total size of the heap, not to what is freed, which becomes expensive on large heaps. Moreover, mark-and-sweep leaves memory fragmented: free blocks are scattered in holes. To mitigate this, compaction was born: moving live objects together to leave a single contiguous free block, exactly what the mark-compact collector does.
Generations: the strategy that makes it fast
An empirical observation — the generational hypothesis — says that most objects die very young: almost every object created in a program lasts milliseconds. The GCs of Java, Python (CPython’s generational collector), Go and JavaScript exploit this by dividing the heap into generations.
New objects go into a young generation, small and collected frequently; those that survive several rounds are promoted to old generations, swept less often. The trick is that collecting a small area full of dead objects costs little and reclaims a lot, while scanning the old generation, where almost everything is still alive, is not worth doing often.
That said, the young generation holds references to old objects and vice versa, so generational GCs keep auxiliary structures (such as remembered sets) to know which old objects point into the young area without scanning the whole heap.
Stop-the-world: the price of reclaiming memory
The most annoying problem of a classic GC is that during collection the memory state must stay stable: if a thread mutates the object graph while it is being walked, the collector can lose references or mark wrong things. That is why many GCs stop all threads during the critical phase: the famous stop-the-world, responsible for those noticeable pauses (or GC pauses) that in Java caused applications to “freeze”.
Concurrent and incremental: don’t stop the world
To reduce those pauses, modern collectors work concurrently: while one part of the program runs, another part of the collection advances in parallel. Techniques such as the read barrier or the write barrier intercept memory accesses so the collector and the program threads do not trip over each other.
Go uses a concurrent tri-color GC (objects are colored white, grey or black depending on their marking state), achieving pauses of a few milliseconds even in heavily loaded services. Recent JVMs (G1, ZGC, Shenandoah) have pushed pauses down to a few milliseconds or even sub-millisecond ranges.
Reference counting: the alternative
Not everything is tracing. Python (besides its generational collector, CPython uses reference counting) and Swift or Objective-C count how many references point to each object; when that count reaches zero, memory is freed instantly, with no global pauses.
The issue is solving cycles: if A points to B and B points to A, both counts never reach zero even though nobody else uses them. That is why Python combines counting with a generational collector that detects cycles, and why adding an object to a list inside itself can leave memory orphaned if not handled carefully.
Why it matters to you
A poorly tuned GC can turn a slow API into a fast one, and a good one, vice versa. Choosing the size of generations, when to promote objects or when to do a full collection are decisions with real impact on performance and latency. And in times of limited memory — like mobiles or low-RAM servers — the collector’s efficiency decides how many applications fit on the same machine.
Next time a program manages millions of objects without you ever touching a free, you now know who is doing the dirty work for you.





