Every time you write a line of code that creates an object, a list or a string of text, that data takes up RAM memory. At some point you stop using it, but the space it occupies is not freed by itself. That is where garbage collection (GC) comes in: the automatic mechanism chosen by many languages so that you do not have to manually delete what you create. This article explains how it works internally in the languages that use it, and why others reject it.
The root of the problem: who owns the memory
In languages like C or C++, the responsibility is on the programmer: when you reserve memory with malloc or new, you are the one who must give it back with free or delete. Forgetting to do so produces memory leaks (the program grows without limit until it exhausts the RAM); doing it twice produces memory corruption. It is a delicate balance that has caused decades of security failures and stability bugs.
Languages with a garbage collector lift that weight off you. In Java, Python, Go, Ruby or JavaScript, the runtime (the environment that executes your code) detects which objects are no longer used and frees their memory. The key technical question is how to know, accurately and without slowing things down too much, when an object is no longer useful to anyone.
Reference counting: the simple solution (and its limits)
The first approach, used by Python, Swift and PHP, is reference counting. Every object carries a counter with the number of references pointing to it. Each time a variable points elsewhere or goes out of scope, that counter drops. When it reaches zero, nobody uses it and the object is freed immediately.
The advantage is immediacy: memory is released the moment it stops being used, with no waiting. But it has a classic problem, the reference cycle: if two objects point to each other and nothing external references them, their counters stay at one even though they are unreachable. To solve it, Python adds a periodic cyclic collector that looks for circular structures and breaks them. Reference counting also hurts performance: every pointer assignment touches a counter, which invalidates CPU caches on modern processors.
Tracing reachability: the mark-and-sweep family
The dominant alternative, used by Java, Go, JavaScript and .NET, starts from a different idea: instead of asking who points to this object, it asks where can this object be reached from. It starts from a set of roots (local variables, stack arguments, static variables, processor registers) and walks the object graph following references. Everything reachable from those roots is alive; everything else is garbage.
That process has two phases. The first, mark, walks the graph and marks each live object. The second, sweep, walks all of memory and returns to the allocator (the manager of free blocks) the objects that were not marked. This approach solves cycles from the start and is very effective, but it has a cost: it requires pausing the program so the state does not change while the graph is traversed. That halt is the famous stop-the-world, and minimizing it is one of the great battles of GC engineering.
Reducing pauses: generational and concurrent collection
An empirical observation supports much of the design: the generational hypothesis. Most objects die young (a temporary variable is created and discarded in milliseconds). That is why the heap is divided into generations: young objects live in a small zone, and those that survive several collections are promoted to older generations. The GC can then collect frequently only the young zone — quickly and cheaply — and rarely touch the old zone, which barely changes.
So that the application does not stop, modern JVMs such as HotSpot use concurrent collectors that run on background threads while your program keeps running. The difficulty is that memory changes while the graph is traversed, so techniques such as write barriers are used: synchronization points that notify the GC whenever a thread modifies a reference, so it can correct its view of the graph. The result is that the pause drops from seconds to milliseconds.
Compaction and fragmentation
When objects of different sizes are freed in scattered positions, memory becomes fragmented: holes remain between live objects, and even though there is total free memory, there may be no contiguous hole for a large object. Many collectors add a compaction phase: they move live objects together to one end, leaving a contiguous block of free space. That improves memory usage and cache locality (objects used together end up physically close), at the cost of moving objects and updating all references.
Go: a case study in pragmatism
Go chose a concurrent, non-generational collector with no compaction, with an explicit goal: keep pauses below a few milliseconds even with terabytes of data. Its central trick is a write barrier known as tricolor: the GC walks the graph in three colors (white=unvisited, grey=in progress, black=processed). The barrier guarantees that a black object never points to an unvisited white object, which allows the program to keep writing memory without corrupting the analysis. It is a perfect example of balancing correctness, throughput and latency.
The opposite path: Rust and ownership without a collector
Rust proves there is a third way: no runtime garbage collector at all. Its ownership and borrowing system is resolved at compile time: the compiler statically infers when each piece of data ends its life and frees its memory when it goes out of scope. There is no GC running, no pauses, and the cost of freeing is deterministic. The price is a steep learning curve: the compiler forces you to be explicit about who owns each piece of data and how it is borrowed. For systems with strict latency requirements, that trade-off is usually worth it.
Conclusion: nothing is free
Garbage collection is not magic: it is engineering that moves complexity in exchange for CPU time, memory and, in the worst case, pauses. Understanding whether your language counts references, traces reachability or resolves memory at compile time helps you choose the right tool and write code that respects how its memory manager works.





