Detailed explanation
A memory leak in Java occurs when an application retains objects that it no longer needs, but the garbage collector still considers them reachable. Garbage collection manages object lifetime based on reachability, not business usefulness. Therefore, Java prevents many manual memory-management errors, but it cannot automatically identify obsolete cache entries, abandoned sessions, or unused listeners. These objects occupy heap space, potentially increasing garbage-collection work and eventually causing an OutOfMemoryError.
Under the hood, the collector traces references starting from GC roots, such as references held by live threads and other JVM-managed roots. Objects reachable through strong reference chains remain eligible to survive collection. For example, static List<Object> cache = new ArrayList<>(); combined with cache.add(o) retains every added object until entries are removed or the owning class becomes unloadable. In a typical application, that class stays loaded for the application's lifetime. The list also retains objects indirectly through its elements, so a small collection entry can keep a much larger object graph alive. Cyclic references alone do not cause leaks: an unreachable cycle can still be collected.
Common causes include unbounded caches, listeners or callbacks never deregistered from long-lived publishers, and ThreadLocal values left on pooled threads. Because pool threads are reused, those values may outlive the request that created them; remove() in a finally block is a common safeguard. Unclosed files, sockets, or database connections are primarily resource leaks, rather than necessarily heap leaks. They may retain Java objects or exhaust external resources, so deterministic cleanup using try-with-resources is important even when garbage collection is available.
Interviewers usually expect both the reachability explanation and a practical diagnosis strategy. Observe whether the live heap keeps growing after comparable collection cycles, then examine a heap dump using a tool such as Eclipse MAT. Look at retained size, dominators, and paths to GC roots to identify the owner keeping unwanted objects alive. Fix that ownership or lifecycle problem through bounded caches, eviction, listener removal, or cleanup. Calling System.gc() or increasing heap size does not remove the retaining references; more heap merely delays failure when retention remains unbounded.
Key points to remember
- GC reclaims unreachable objects, not reachable objects that application logic no longer needs.
- A long-lived owner can retain a large object graph through strong references.
- Typical causes include unbounded static collections, forgotten listeners, and uncleared ThreadLocal values.
- Unclosed resources can exhaust native or external capacity even without a growing Java heap.
- Use heap dumps and paths to GC roots to locate unwanted retention and fix its lifecycle.
Common mistakes candidates make
- Claiming that Java cannot have memory leaks because it has garbage collection.
- Blaming circular references alone; Java can collect cycles that are unreachable from GC roots.
- Treating System.gc(), a larger heap, or setting one local variable to null as a fix while other strong references remain.
Likely follow-up questions
How do you distinguish a memory leak from normal high memory usage?
High memory usage alone does not prove a leak; the application may legitimately retain a large working set. A rising post-GC live heap under comparable workloads suggests unwanted retention, which heap analysis can confirm.
Would using WeakHashMap fix a leaking cache?
WeakHashMap allows entries to disappear when their keys are no longer strongly reachable elsewhere. It is not a general bounded-cache replacement, and a value that strongly references its key can prevent that key from being collected.
Why can ThreadLocal cause leaks in thread pools?
A pooled thread can live much longer than a request and retain its ThreadLocal values between tasks. ThreadLocal map keys are weak references, but values are strongly referenced and stale entries are not necessarily cleared immediately; call remove() in finally when the task finishes.
Interview tip: Start with the difference between reachability and usefulness, explain the static-list example, and finish with one diagnostic technique and a lifecycle-based fix.
