A static cache grows without bound despite the garbage collector
A request handler caches every Session it has ever parsed so that repeat lookups are fast. The service runs for days and eventually dies with OutOfMemoryError: Java heap space, even though the garbage collector runs constantly and each Session is finished with long before then.
Constraints:
- repeat lookups of an active session must stay fast — deleting the cache outright is not the fix
- assume a
Sessionis large and no other object in the service refers to it after its request ends
public class SessionRegistry {
private static final Map<String, Session> CACHE = new HashMap<>();
public static Session lookup(String id) {
return CACHE.computeIfAbsent(id, SessionRegistry::parse);
}
private static Session parse(String id) { /* expensive */ }
}
Find and fix the bug.
The static final map is a GC root holding a strong reference to every Session ever parsed, so no entry ever becomes unreachable and the collector cannot reclaim it — a leak by reachability, not a GC bug. Fix it by bounding the cache: an LRU via LinkedHashMap.removeEldestEntry, or a WeakHashMap whose entries clear once no strong reference to the key remains.
- ✗Believing a garbage collector makes a memory leak impossible in Java
- ✗Blaming the collector or the generation sizing when the object is still reachable
- ✗Treating
System.gc()or a bigger heap as a fix for an unbounded cache
- →How would a heap dump show which GC root is retaining the sessions?
- →When is a
WeakHashMapthe wrong cache and a bounded LRU the right one?
Solution
public class SessionRegistry {
private static final int MAX_ENTRIES = 10_000;
private static final Map<String, Session> CACHE =
Collections.synchronizedMap(new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Session> eldest) {
return size() > MAX_ENTRIES; // bound the cache: evict the eldest
}
});
public static Session lookup(String id) {
return CACHE.computeIfAbsent(id, SessionRegistry::parse);
}
}
Diagnosis. A garbage collector reclaims only unreachable objects. CACHE is a static final field — a GC root — and it holds a strong reference to every Session ever parsed. The objects are reachable, so by definition they are not garbage and the collector must keep them. The heap grows monotonically; System.gc() and a bigger -Xmx only postpone the crash.
Direction of the fix. The leak is cured by bounding retention, not by tuning the GC:
- A bounded LRU (
LinkedHashMapin access order +removeEldestEntry) — a hard ceiling on size, with hot sessions still cached. - A
WeakHashMap— the entry disappears once no strong reference to the key remains; suitable when something else holds the live keys. SoftReferencevalues — cleared under memory pressure; a safety valve, not a substitute for a bound.