What causes memory leaks in Java despite having a garbage collector?
Quick Answer
Java's GC only reclaims objects that are unreachable. A memory leak still happens whenever code keeps an unintended strong reference to an object it no longer needs, preventing collection even though the object is logically 'done'. Common causes: unbounded caches without eviction, listeners/callbacks never unregistered, ThreadLocal values not removed in pooled threads, inner classes holding an implicit outer-instance reference longer than intended, and static collections that only ever grow.
Detailed Answer
Garbage collection reclaims objects that are unreachable from any GC root. It does nothing to help if your code still holds a reference to an object it no longer actually needs. A "Java memory leak" is really an unintentional reachability leak: something keeps pointing at an object long after it's logically garbage, so it silently accumulates and eventually causes OutOfMemoryError.
Common real-world causes:
- Unbounded caches: a
HashMapused as a cache that only ever grows, with no eviction policy or size cap. Every entry ever added stays reachable forever.
static Map<String, byte[]> cache = new HashMap<>(); // never removes entries — grows forever
Fix: use a bounded cache — an LRU-evicting cache, SoftReference values, or a library like Caffeine.
-
Unregistered listeners/callbacks: registering a listener on a long-lived object, like a UI component or event bus, and never removing it. The long-lived publisher keeps the listener, and everything it references, alive indefinitely.
-
ThreadLocalvalues not cleaned up in thread pools: since pooled threads are reused, a value set viaThreadLocal.set()and neverremove()-d persists on that thread indefinitely, referenced by the thread's internalThreadLocalMaplong after the task that set it has finished. -
Non-static inner classes: each instance implicitly holds a reference to its enclosing outer instance. If you hand out a long-lived inner-class instance — say, storing it in a static collection — it transitively keeps the entire outer object alive too, often unexpectedly.
-
Static fields holding large collections:
staticfields live for the entire JVM lifetime, tied to the class rather than any instance. Accumulating data in a static collection without ever clearing it is a straightforward, common leak.
Diagnosis: heap dumps (via jmap/VisualVM/Eclipse MAT) showing an ever-growing count of a particular object type, and GC logs showing the old generation steadily climbing even after full GCs, are the classic symptoms of a reachability leak rather than a genuine allocation-rate problem.