Introduction Your program keeps creating objects.
Every function call, every loop iteration, every parsed JSON response produces new ones.
You don't manually delete most of them.
You've never written a line of code that says "free this memory now." And yet your application doesn't immediately exhaust all available RAM and crash.
So who cleans everything up?
The answer is a garbage collector, a piece of the runtime that runs quietly in the background, deciding what your program no longer needs and reclaiming that memory for future use.
Most developers interact with it only when something goes wrong: an unexpected pause, a memory leak, or an out-of-memory error that shouldn't be happening.
Understanding how it actually works turns those confusing moments into solvable problems.
And as a bonus, the core algorithm is simple enough to build yourself.
We'll do that by the end of this article. --
1.
The Memory Problem Every time your program creates an object, the runtime allocates a chunk of memory to hold it.
A string, a dictionary, a class instance: they all need memory, and that memory has to come from somewhere.
The somewhere is a region called the heap, a pool of memory that the program draws from as it runs.
When you create an object, the runtime finds a suitable slot in the heap and reserves it.
When that object is no longer needed, that slot should be freed so it can be used for something else.
In languages like C, you manage this manually.
You allocate memory when you need it, and you free it when you're done.
This gives you control, but it creates two classic failure modes.
Free memory too early and you have a dangling pointer, a reference to memory that's now being used for something else.
Forget to free it at all and you have a memory leak: the program slowly consumes more and more memory until it runs out.
Automatic memory management exists to eliminate these failure modes.
Instead of relying on the programmer to track every allocation and release, the runtime watches what the program is doing and cleans up on its behalf.
The question is: how does it know what's safe to clean up? --
2.
The Simplest Idea: Reference Counting The most intuitive approach is to count how many references point to each object.
A reference-counting runtime keeps track of how many references currently point to an object.
When that count reaches zero, nothing in the program can reach the object anymore, and the runtime can reclaim its memory immediately.
In CPython, the standard Python implementation, this is the primary mechanism.
Every Python object carries a reference count.
Each time a new reference to it is created, the count goes up.
Each time a reference is removed or goes out of scope, the count goes down.
When it hits zero, the memory is released on the spot, without waiting for a separate collection phase.
The exact numbers vary depending on context and interpreter internals, so don't rely on specific values.
What matters is the pattern: the count goes up when a new reference is created and down when one is removed.
Reference counting is elegant in its simplicity.
It distributes the work of garbage collection across the program's normal execution: every assignment and deletion carries a small bookkeeping cost, but there's no separate "stop everything and collect garbage" moment for objects that die by reference count.
There's just one problem. --
3.
The Problem With Reference Counting Consider two objects that each hold a reference to the other.
After and , the variables in your code no longer point to these objects.
From your program's perspective, they're gone.
But from the runtime's perspective, A still holds a reference to B, and B still holds a reference to A.
Each object's reference count is one.
Neither will ever reach zero.
This is the circular reference problem.
Two objects keeping each other alive, even when the rest of the program has moved on.
The memory they occupy is effectively leaked, not because the programmer forgot to delete them, but because pure reference counting has no way to detect or collect cycles.
This is a real limitation in CPython, and it's why CPython includes a second mechanism on top of reference counting: a cyclic garbage collector that periodically searches for isolated reference cycles among container objects and reclaims them.
For most programs most of the time, reference counting handles cleanup.
The cyclic collector handles the cycles that reference counting cannot.
Other Python implementations may handle this differently.
But reference counting is not the only approach to automatic memory management.
There's a more general algorithm that handles cycles naturally. --
4.
Mark and Sweep Instead of tracking reference counts, tracing collectors ask a different question: starting from what the program can currently access, what objects can be reached?
Mark-and-sweep is one concrete way to implement this idea.
The idea begins with a concept called roots: the starting points of the reachability search.
Roots are the references the program can directly access at a given moment: local variables in active stack frames, global variables, static fields.
Anything the program holds onto directly.
From the roots, the collector follows every reference it can find.
Object A points to B, so B is reachable.
B points to C, so C is reachable.
The collector visits every object it can reach and marks it.
Then comes the sweep: the collector walks through all known objects.
Any object that wasn't marked is unreachable.
The program can never access it again.
Its memory can be safely reclaimed.
Mark-and-sweep handles circular references cleanly.
If D and E reference each other but nothing reachable points to either of them, neither gets marked.
Both get swept.
The cycle doesn't protect them.
The trade-off is that mark-and-sweep isn't free.
During collection, the collector has to traverse potentially large object graphs.
Depending on the implementation, this may require pausing the application while it runs. --
5.
Why Generational GC Exists Here's an observation that turns out to be remarkably consistent across real programs: most objects die young.
A temporary variable inside a function lives for a fraction of a second.
A string built to format a log message exists for one call and is never seen again.
Meanwhile, a database connection pool or a configuration object might live for the entire lifetime of the application.
This pattern, known as the generational hypothesis, is reliable enough that garbage collectors are specifically designed around it.
Instead of collecting all objects together, the heap is divided into generations.
New objects start in the youngest generation.
If an object survives a collection cycle, it gets promoted to an older generation, which is collected less frequently.
The result is that most collection cycles are fast: they sweep through a small, young generation where most objects are already dead, find a lot of garbage quickly, and finish.
Long-lived objects in older generations are rarely disturbed.
Modern Java collectors such as G1 and the current generational ZGC use generational techniques, although the exact implementation varies between collectors and JDK versions.
Most JavaScript engines use generational collection as well, with young-generation collection ("scavenging") happening frequently and full collections happening rarely. --
6.
Why GC Can Pause Your Program A garbage collector needs to understand the state of all objects and all references.
If the program is modifying those references while the collector is traversing them, the collector's view of the world becomes inconsistent.
An object that was reachable at the start of the traversal might no longer be reachable by the end.
A newly allocated object might not get marked at all.
The simplest solution is to pause all application threads while the collector runs: a stop-the-world pause.
The application freezes, the collector does its work with a stable view