What causes memory leaks in Java despite having a garbage collector?

9 minadvancedmemory-leaksgcbest-practices

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:

  1. Unbounded caches: a HashMap used 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 (e.g., an LRU-evicting cache, or SoftReference values, or a library like Caffeine).

  1. Unregistered listeners/callbacks: registering a listener on a long-lived object (e.g., a UI component or event bus) and never removing it — the long-lived publisher keeps the listener (and everything it references) alive indefinitely.

  2. ThreadLocal values not cleaned up in thread pools: since pooled threads are reused, a value set via ThreadLocal.set() and never remove()-d persists on that thread indefinitely, referenced by the thread's internal ThreadLocalMap long after the task that set it has finished.

  3. 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 (e.g., store it in a static collection), it transitively keeps the entire outer object alive too, often unexpectedly.

  4. Static fields holding large collections: static fields live for the entire JVM lifetime (tied to the class, not 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 pointing at a reachability leak rather than a genuine allocation-rate problem.