skip to content

Why can declaring `inner` on a nested class cause memory leaks, and how does choosing nested instead help?

level: middleimportance: should knowfreq 45%

answer

  1. inner adds a synthetic strong outer field
  2. Leak when inner outlives outer
  3. Nested holds no outer reference
  4. Pass a snapshot, not the whole outer
  5. Android Activity leak is the classic case

basics

~20 s

An inner class secretly holds a reference to its outer object, so as long as the inner instance is alive, the outer can't be garbage-collected. A plain nested class holds no such reference, so it lets the outer be freed.

solid answer

~40 s

Every `inner` instance carries an implicit, strong reference to the enclosing `Outer` instance. If that inner instance outlives the outer's intended lifecycle — e.g. it is captured by a long-lived callback, a static cache, a `Handler`, or a coroutine — the outer object (and everything it transitively holds) cannot be garbage-collected. On Android this classically leaks an `Activity`/`Fragment`. Switching to a plain **nested** class removes the hidden reference; if the nested class still needs some outer data, pass only what it needs (e.g. an application context, an immutable snapshot) so you control exactly what is retained. Same logic applies to anonymous/local classes and non-capturing lambdas: capture deliberately, not implicitly.

go deeper

for a junior

Knows inner holds the outer alive and nested does not.

for a middle

Explains the synthetic strong reference and the lifetime-mismatch leak, and proposes passing a snapshot.

for a senior

Connects to Android lifecycles, anonymous-class/lambda capture, and WeakReference trade-offs.

for a principal

Reasons about ownership/lifetime as an API-design concern, choosing capture strategy to make retention explicit and auditable across a codebase.

## The hidden reference When you write `inner`, the compiler adds a synthetic field holding the enclosing `Outer` instance (so `this@Outer` works). That reference is **strong**: the garbage collector treats the outer as reachable for as long as the inner instance is reachable. ## How a leak forms A leak happens when the inner instance's lifetime exceeds the outer's intended lifetime: ```kotlin class Screen { // imagine an Android Activity val bigState = ByteArray(10_000_000) inner class Ticker : Runnable { // holds Screen implicitly override fun run() { /* uses bigState */ } } fun start() = GlobalScope_like_executor.schedule(Ticker()) // long-lived } ``` If the executor keeps the `Ticker` for minutes, the whole `Screen` (including `bigState`) stays in memory even after the screen is gone. ## The fix: prefer nested + explicit capture ```kotlin class Screen { private val bigState = ByteArray(10_000_000) class Ticker(private val snapshot: Int) : Runnable { // nested, no Screen ref override fun run() { /* uses only snapshot */ } } fun start() = executor.schedule(Ticker(bigState.size)) } ``` The nested `Ticker` holds only the small `snapshot`, never `Screen`, so `Screen` can be collected. ## Related cases - Anonymous objects and local classes inside an instance method **also** capture the enclosing instance — same risk. - A lambda that references no outer state captures nothing; one that references `this.x` captures the instance. - A `WeakReference` is an escape hatch when you genuinely need the outer but must not keep it alive. ## Key takeaways - `inner` = strong implicit outer reference = leak risk if it outlives the outer. - Default nested + pass only the data you need. - The same capture logic governs anonymous classes and lambdas.

  • Does using a non-inner nested class fully eliminate capture concerns?
    It removes the implicit outer reference, but you must still avoid explicitly passing long-lived references; capture only what is needed.
  • When is a WeakReference appropriate?
    When the nested/inner object genuinely needs the outer but must not keep it alive; the GC can clear it once the outer is otherwise unreachable.

An inner class is a balloon tied to the outer object by string; until you let go of the balloon, the heavy outer object can't float away (be collected).

saying these in an interview costs you the question

  • Saying inner classes only cost a few bytes and never matter
  • Claiming the GC collects the outer regardless of inner references
  • Not knowing anonymous classes/lambdas can also capture the outer
  • Suggesting `internal` visibility fixes the leak
  • Believing nested classes can't access any outer data even when explicitly passed

context