skip to content

Java has a garbage collector, so how can a Java program still leak memory?

level: juniorimportance: must knowfreq 70%

answer

  1. GC frees only unreachable objects
  2. Leak = unintended reachability from a GC root
  3. Reference you forgot to drop, not a forgotten free()
  4. Symptom: retained heap grows, survives GC
  5. Fix: make it unreachable (remove/deregister/close)

basics

~20 s

The garbage collector only frees objects nothing else points to. If your code keeps a reference to an object you no longer use, the collector cannot free it. The memory is still 'reachable', just wasted. That is a Java memory leak.

solid answer

~50 s

Java's garbage collector reclaims only unreachable objects: objects that can no longer be reached by following references from a GC root (like a static field, a live thread's stack, or an active local variable). A leak happens when an object is no longer needed by the program but is still reachable, so the collector is forbidden from freeing it. Unlike a C leak (where you forget to free), a Java leak is unintended retention: a reference you forgot to drop. The classic symptom is a steadily growing retained heap that survives garbage collection and eventually triggers OutOfMemoryError. Common culprits are unbounded caches or static collections, listeners never deregistered, and ThreadLocals in pooled threads. The fix is always the same in spirit: drop the reference (null it, remove from the collection, deregister, or close), so the object becomes unreachable and collectible.

go deeper

for a junior

Can state that the GC frees objects nothing points to, and that a leak means you kept a reference you no longer need. Knows it leads to OutOfMemoryError.

for a middle

Explains reachability vs. usefulness, can name GC roots, and connects 'still reachable' to 'cannot be collected'. Can name at least two concrete leak patterns.

for a senior

Articulates the reachability model precisely, distinguishes a Java leak from a C leak, describes the retained-heap symptom and the heap-dump/GC-root-path diagnosis, and the general fix.

for a principal

Frames leaks as an ownership/lifecycle design problem: who holds the long-lived reference and for how long. Sets conventions (bounded caches, weak references, lifecycle hooks) and observability to catch slow retention growth before OOM.

## The core idea A **memory leak** is memory the program has allocated but can no longer use *and* can no longer reclaim. In manually-managed languages like C, a leak means you allocated memory with `malloc` and forgot to `free` it. In Java you never call `free` — the **garbage collector (GC)** does it for you — so leaks look different. ## What the garbage collector actually does The GC's job is to find and delete objects that are **unreachable**. An object is **reachable** if you can get to it by starting at a **GC root** and following references (object-to-object pointers). **GC roots** are the starting points the collector trusts as 'definitely alive': - **static fields** of loaded classes, - local variables and parameters on the **stack of any live thread**, - active **JNI/native** references, - a few JVM internals. The collector does a **reachability analysis**: start at every root, walk every reference, mark everything you can touch as live. Anything *not* marked is unreachable and gets freed. This is the key rule: **the GC frees an object only when nothing reachable points to it.** ## So what is a Java leak? A Java leak is **unintended reachability**: an object you are logically done with is *still* reachable from some GC root, so the collector is *not allowed* to free it. The program no longer uses the object, but it cannot be collected because a reference is still hanging on somewhere. Note the asymmetry: you don't have to do anything 'wrong' like a double-free. You just have to **forget to drop a reference**. The leak is silent — the object sits in memory, fully retained, doing nothing. ## Why it grows Leaks matter when the unwanted-but-retained objects **accumulate over time**: every request adds an entry to a cache that is never evicted, every connection registers a listener that is never removed, every pooled thread leaves a value in a `ThreadLocal`. The **retained heap** (the total memory kept alive because of these references) climbs steadily. Eventually the heap fills, the GC works harder and harder for less and less free space, and you get an `OutOfMemoryError: Java heap space`. ## Classic leak patterns (rooted in reachability) Every pattern below is the same root cause — a reference from a long-lived GC root to a short-lived object: 1. **Unbounded static collections / caches** — a `static Map` or `List` that you only ever add to. Statics are GC roots, so everything in them is permanently reachable. 2. **Listeners / callbacks never deregistered** — you `addListener(this)` but never `removeListener`. The event source (often long-lived) keeps your object alive. 3. **ThreadLocals in pooled threads** — a thread-pool thread never dies, so a value you stored in a `ThreadLocal` and never `remove()`d stays reachable for the life of the pool. 4. **Unclosed resources** — streams, connections, and sessions that aren't closed keep buffers and native handles alive. ## How you detect and fix it **Detect:** watch for the heap's *live* (post-GC) size trending upward over time; capture a **heap dump** and look at which objects have the largest **retained size** and what GC-root path keeps them alive. **Fix:** make the object unreachable again — remove it from the collection, deregister the listener, call `ThreadLocal.remove()`, or close the resource (ideally with try-with-resources). Once no GC root can reach it, the next collection frees it. ## The mental model to carry away 'GC frees what you can't reach.' A leak is therefore **you keeping a reference you no longer want.** Find the unwanted reference, drop it, and the leak is gone.

  • What is a GC root, and why does it matter for leaks?
    A GC root is a starting point the collector treats as definitely alive: static fields, live thread stacks, active local variables, JNI references. The collector keeps anything reachable from a root. A leak exists precisely when an unwanted object is still reachable from some root.
  • Will calling System.gc() free leaked memory?
    No. System.gc() only suggests a collection, and the collector can still free only unreachable objects. A leaked object is still reachable, so no collection — forced or not — can reclaim it. You must drop the reference first.

saying these in an interview costs you the question

  • Claiming Java cannot leak because it has a garbage collector
  • Saying the leak is unfreed memory like a C malloc/free bug
  • Confusing 'unused' with 'unreachable' — the GC only cares about reachability
  • Thinking System.gc() fixes a leak (it can't free still-reachable objects)

context