Every object your program creates takes up memory, and garbage collection is how that memory gets handed back once nothing needs the object any more. How it works explains why some apps slowly eat memory and others pause for no obvious reason.
What counts as garbage
When a program creates an object, such as a list or an instance of a class, the runtime sets aside space for it on the heap, the area of memory used for data that outlives a single function call. Your code then holds references to that object: a variable, a field on another object, an entry in a list.
An object becomes garbage once the program has no way left to reach it. How recently it was used makes no difference: an old object that something still refers to stays alive.
cat = {"name": "Tuna"}
cat = {"name": "Mochi"}
# The Tuna dict has no references left.
# Nothing can ever read it again: it is garbage.The search for reachable objects starts from what are called roots: local variables on the call stack, global and static variables, and a few things the runtime itself holds on to. Anything a root points to is alive, anything those objects point to is alive, and so on. Everything else can go.
Why memory needs cleaning up
Memory is finite. If unused objects are never removed, the heap keeps growing for as long as the program runs. At first the app just uses more RAM than it should. Then the operating system starts swapping memory to disk and everything slows down. Eventually an allocation fails and the program crashes with an out-of-memory error.
A long-running server is where this hurts most. A tiny leak per request is invisible in a quick test, then takes the service down after a few days in production.
How a collector finds garbage
There are two main families of technique, and most runtimes mix them.
Mark and sweep
A tracing collector works out what is alive by following references:
- Mark: start at the roots and follow every reference, marking each object reached.
- Sweep: walk the heap and free every object that was not marked.
Here is one collection, from the roots out:
One mark and sweep collection
Step 1 of 6: The collector starts at the roots: the program's variables.
Because it only cares about reachability, tracing handles cycles for free. Two objects that refer to each other but that nothing else can reach are still garbage. Java's and .NET's collectors are tracing collectors, and many also compact the heap afterwards, sliding the survivors together so new objects can be allocated in one contiguous block.
Reference counting
The other approach keeps a counter on each object: how many references point to it. Assigning a reference adds one; dropping one subtracts one. When the count reaches zero, the object is freed straight away.
CPython, the standard Python interpreter, works this way, which is why most Python objects disappear the moment the last variable stops pointing at them. The weak spot is cycles: two objects pointing at each other never reach zero. So CPython also runs a separate cycle collector, exposed through the gc module:
import gc
class Node:
def __init__(self):
self.other = None
a = Node()
b = Node()
a.other = b
b.other = a
del a, b # counts never reach zero
gc.collect() # the cycle collector frees themYou rarely need to call gc.collect() yourself; it runs on its own. The example just makes the cycle visible.
Generations
Most objects die young: a temporary string built inside a loop, a list returned and thrown away. Runtimes such as the JVM and .NET exploit this by splitting the heap into generations. New objects go into a young generation that is collected often and cheaply. Objects that survive a few collections are promoted to an older generation that is checked less often. The collector spends its effort where garbage is most likely.
Cleaning by hand: C and C++
In Java, C# and Python, the collector does all of this automatically. In C and C++ there is no collector by default, and the programmer frees every object they create.
#include <stdlib.h>
void fill_bowl(void) {
int *bowl = malloc(10 * sizeof(int));
if (bowl == NULL) return;
bowl[0] = 3;
free(bowl); /* hand it back */
}Every malloc needs exactly one matching free, and that is where the bugs come from:
- Memory leak: forget the
free, often on an early return or an error path, and the memory is lost until the process exits. - Use after free: read or write memory after freeing it, and you get corrupted data or a security hole.
- Double free: free the same block twice and the allocator's bookkeeping breaks, often crashing later somewhere unrelated.
Modern C++ reduces this with RAII and smart pointers. A std::unique_ptr frees its object when the pointer goes out of scope, so the cleanup is written once, in the type, instead of at every exit. That is still not garbage collection: nothing scans memory, and the free happens at a known point in the code.
The cost of automatic cleaning
Garbage collection removes whole classes of bugs, but it is not free.
- Pauses: some phases stop the program's threads while the collector works. Modern collectors do most of their work alongside the program, but pauses can still show up as latency spikes in a game or a busy API.
- Overhead: the collector uses CPU time, and a collected heap usually needs headroom beyond the live data to run efficiently.
- Unpredictable timing: you don't control when an object is freed.
This is why operating systems, game engines, embedded firmware and other latency-sensitive code are often written in languages that manage memory by hand or, like Rust, check ownership at compile time. For most application code, the trade pays off.
Common mistakes
- Assuming a collected language can't leak. The collector only frees what is unreachable. A global list, a cache with no size limit or an event listener that is never removed keeps objects reachable for ever. That is a leak, and the collector cannot help.
- Relying on the collector to close things. Files, database connections and sockets should be closed explicitly, with
within Python,usingin C# or try-with-resources in Java. Finalisers may run late, or never. - Calling the collector by hand to fix performance.
gc.collect()orSystem.gc()sprinkled through code usually makes things slower. Measure with a profiler first.
Key takeaways
- An object is garbage when nothing reachable from the program's roots refers to it.
- Tracing collectors mark what is reachable and sweep the rest; reference counting frees an object when its count hits zero but needs help with cycles.
- Java, C# and Python collect garbage automatically; in C and C++ you free memory yourself.
- A garbage collector prevents most leaks, but references you keep by mistake still leak.
- Automatic collection costs some CPU, memory and occasional pauses, in exchange for far fewer memory bugs.