skip to content

Why can a non-static inner class cause a memory leak, and how do you avoid it?

level: seniorimportance: should knowfreq 47%

answer

  1. implicit this$0 keeps the outer reachable
  2. reachable means GC can't collect it
  3. leak when inner outlives outer (listeners/threads/caches)
  4. fix: make it static + pass only needed data
  5. alternatives: deregister, WeakReference

basics

~20 s

An inner-class object secretly holds a reference to its outer object. If the inner object lives a long time (e.g., a listener or callback), it keeps the whole outer object alive in memory even after you're done with it. Use a static nested class to break that link.

solid answer

~40 s

Every non-static inner instance contains an implicit reference to its enclosing instance. Garbage collection frees an object only when nothing reachable still points to it, so as long as the inner object is reachable, the outer object cannot be collected. This bites when the inner object outlives its logical owner: a long-lived listener, a thread/Runnable, a cache entry, or a callback registered with a global registry will pin the outer object (and everything it references) in memory — a leak. The fixes: make the nested class `static` so it carries no outer reference, passing in only the specific data it needs; or, where you must reference the outer object loosely, hold it through a `WeakReference`; and always deregister listeners/callbacks when done. Effective Java's default-to-static rule exists precisely to prevent this accidental retention.

code

java · 20 lines
java
// LEAK-PRONE: inner listener pins the whole Screen via this$0
class Screen {
    private final byte[] bigBuffer = new byte[10_000_000];
    void register(EventBus bus) {
        bus.add(new Listener() {           // non-static (anonymous) inner class
            public void onEvent() { repaint(); }  // captures Screen.this
        });
    }
    void repaint() { /* uses bigBuffer */ }
}

// SAFER: static class holds only a weak reference to what it needs
class SafeListener implements Listener {
    private final java.lang.ref.WeakReference<Screen> ref;
    SafeListener(Screen s) { this.ref = new java.lang.ref.WeakReference<>(s); }
    public void onEvent() {
        Screen s = ref.get();
        if (s != null) s.repaint();   // outer can be GC'd when nothing else holds it
    }
}

go deeper

for a junior

Knows the inner class holds the outer object and that this can keep memory around longer than expected.

for a middle

Explains reachability and names typical leak scenarios (listeners, long-lived tasks) and the static-class fix.

for a senior

Diagnoses via heap dump / this$0, weighs static-plus-snapshot vs WeakReference vs deregistration, and applies the right fix per lifetime.

for a principal

Sets codebase conventions (default static, lifecycle-bound registration, lint rules), and reasons about retention across subsystems and frameworks (UI/Android, executors).

## Background terms - **Garbage collection (GC)**: Java automatically frees objects that are no longer **reachable** — meaning no chain of references from a live root (a thread stack, static field, etc.) leads to them. If something still references an object, GC keeps it. - **Memory leak (in managed languages)**: not unfreed memory in the C sense, but objects that *stay reachable* long after they are logically useless, so they are never collected and memory grows. - **Non-static inner class**: holds an **implicit reference to its enclosing (outer) instance**. ## The mechanism of the leak When you create an inner-class object, it stores a hidden field pointing at the outer object. Now consider a chain: some long-lived structure → inner object → (implicit) outer object → all of the outer object's fields. As long as the long-lived structure keeps the inner object reachable, **the entire outer object subtree is reachable too** and cannot be collected. The danger appears whenever the inner object's lifetime exceeds the outer object's intended lifetime. Classic cases: 1. **Event listeners / callbacks**: you register an inner-class listener with a global event bus or UI widget and never remove it. The bus holds the listener → holds the outer object forever. 2. **Threads and `Runnable`s**: an inner-class `Runnable` handed to a long-running executor keeps the outer alive until the task finishes (or forever, for endless tasks). 3. **Caches / collections** that keep entries: an inner-class value pins its outer. 4. **Anonymous classes and non-static-context lambdas that capture `this`** — anonymous classes are inner classes too, and a lambda inside an instance method that uses an instance member captures the enclosing `this`. ## How to detect it A heap dump (via a profiler such as VisualVM, Eclipse MAT, or async-profiler) shows the inner object with a synthetic field commonly named `this$0` pointing to the outer instance; the retained-size of the outer object is held through that path. ## Fixes, in order of preference 1. **Make it `static`.** A static nested class has no outer reference. Pass in only the exact values it needs (e.g., a final snapshot of one field) instead of the whole outer object. This is the Effective Java default and removes the leak by construction. 2. **Deregister.** If you must register a listener, remove it when the owner is destroyed (`removeListener`, lifecycle `onDestroy`, `try/finally`). 3. **Hold the outer weakly.** When a static class still needs to call back into the outer object, store it in a `java.lang.ref.WeakReference`; GC may then reclaim the outer once nothing else holds it, and the callback null-checks the weak ref. 4. **Prefer top-level or static lambdas** that capture nothing (or only locals), avoiding `this` capture. ## Trade-off Making the class static can mean threading a few fields through its constructor — slightly more boilerplate in exchange for correct lifetime semantics. That trade almost always favors `static`.

  • Does a lambda inside an instance method also capture the enclosing instance?
    Only if it uses an instance member (a field or instance method, or `this`). Such use captures the enclosing `this`, with the same retention risk. A lambda that touches only local variables or static state captures no `this`.
  • Why doesn't simply increasing the heap fix the leak?
    The retained objects stay reachable indefinitely, so they accumulate without bound. A larger heap only postpones the OutOfMemoryError; the reference itself must be removed.

saying these in an interview costs you the question

  • Claiming Java can't leak because it has GC
  • Thinking the leak is about the inner object's own size rather than the retained outer subtree
  • Believing only explicit static fields cause retention, ignoring the implicit outer reference

context