Skip to content

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:

code
young generation    new objects, collected often and quickly
old generation      survivors, collected rarely

A 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.

CollectorOptimises
Serialsimplicity, small heaps
Parallelthroughput, longer pauses
G1predictable pauses on large heaps
ZGC, Shenandoahpauses 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.

java
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.