26 · 32 · 5 min
Memory nobody has to free
In C, every allocation needs a matching free, and getting it wrong gives leaks, double frees, or use after free. Managed languages take the job away.
Reachability
An object is collectable when nothing can reach it. The collector starts from roots, local variables on every stack, static fields, and active threads, and follows every reference. Whatever is not reached is garbage.
That is the important distinction: it is not "no references", it is "not reachable from a root". Two objects referencing each other with nothing else pointing at them are both garbage, which is why reference counting alone is insufficient.
Generational collection
An observation that holds across almost all programs: most objects die young. A request handler allocates a few dozen objects that are garbage milliseconds later, while the cache lives for days.
So the heap is split:
young generation new objects, collected often and quickly
old generation survivors, collected rarelyA young collection walks only the small live set and copies it out, so its cost is proportional to what survived rather than to what was allocated. Allocation becomes little more than a pointer bump.
Pauses
Most collectors must stop the application briefly to move objects safely, and the pause is what actually matters in production.
| Collector | Optimises |
|---|---|
| Serial | simplicity, small heaps |
| Parallel | throughput, longer pauses |
| G1 | predictable pauses on large heaps |
| ZGC, Shenandoah | pauses under a millisecond |
A 4 second pause on an 8GB heap is a timeout for every request in flight, which is why low pause collectors exist and why they trade some throughput for it.
You can still leak
Garbage collection prevents leaks from forgetting to free. It does not prevent leaks from remembering too much.
static final Map<String, Session> cache = new HashMap<>();A static map nothing ever removes from grows forever, and every entry is reachable, so nothing is collectable. The usual culprits are caches without eviction, listeners never unregistered, and long lived collections that only ever grow.
What to do about it
Mostly nothing. Do not call System.gc(), which is a hint and often harmful. Do not tune flags without measuring.
Do release references you no longer need, close resources explicitly with try-with-resources rather than relying on finalisers, and bound every cache.