Card 07/ 09

GotchaDifficulty: Advanced1 min

A Memory Leak in a Language That Collects Memory

A service that has run for nine days is using eleven gigabytes. Restarting it fixes the problem for another nine days. Nothing in the code allocates anything unusual.

java
class SessionRegistry {
    private static final Map<String, Session> active = new HashMap<>();

    static void login(String id, Session s) { active.put(id, s); }
}

There is no logout. Every session ever created is still in that map, the map is a static field, and a static field is a garbage collection root. Every one of those sessions is reachable, so every one of them is alive.

This is the same rule as the four-line example, in a setting that looks nothing like it: the collector has not changed its mind about anything, and the only question is still whether a path from a root reaches the object. Here it does, nine days of them.

The places this shape turns up
HolderWhat it accumulates
A static collection with no removalEverything ever added
A listener registered and never removedThe listener and everything it points at
A cache with no size limit or expiryEvery key ever requested
A ThreadLocal in a pooled threadOne object per thread, for the life of the pool

Give every long-lived collection a removal path before it has anything in it — a logout, an eviction policy, a bounded cache. A map that only ever grows is a leak that has not had time yet.