skip to content

From a performance and memory-management standpoint, what is the difference between a capturing and a non-capturing lambda, and what risks does capturing `this` introduce?

level: principalimportance: nice to knowfreq 35%

answer

  1. Non-capturing -> singleton, ~zero allocation (invokedynamic)
  2. Capturing -> per-evaluation object with captured fields
  3. Field/instance-method use captures 'this' implicitly
  4. Stored capturing lambda pins outer object = listener leak
  5. Mitigate: capture a value not 'this'; unregister; hoist

basics

~20 s

A non-capturing lambda uses nothing from outside, so the runtime can reuse a single shared instance. A capturing lambda must hold the captured values, so a new object is created and it keeps those values (including this) alive, which can cause memory leaks.

solid answer

~50 s

A **non-capturing** lambda references no enclosing state (no locals, no `this`). Under `invokedynamic`, the JVM can build a single stateless instance once and reuse it on every evaluation — effectively zero per-call allocation. A **capturing** lambda must carry its captured values as fields, so each evaluation that captures fresh values generally allocates a new object. The deeper risk is that capturing an instance field — or `this` directly — captures the **enclosing `this`** reference, so the lambda keeps the whole outer object reachable. If that lambda is stored somewhere long-lived (an event-bus subscription, a static registry, a cache), it pins the outer object (and its transitive graph) in memory: a classic listener leak. Mitigations: prefer non-capturing lambdas where possible; capture a single needed value into a local instead of the whole `this`; unregister listeners; and for hot paths, avoid per-call capture by hoisting stable lambdas.

code

java · 12 lines
java
class Service {
    Config config;                 // instance field
    void register(EventBus bus) {
        // BAD: captures 'this' (via this.config) -> bus pins the whole Service forever
        bus.subscribe(e -> handle(e, this.config.timeout));

        // BETTER: capture only the needed value -> no 'this' captured
        int timeout = this.config.timeout;
        bus.subscribe(e -> handle(e, timeout));
    }
    void handle(Event e, int timeout) { /* ... */ }
}

go deeper

for a junior

Understands that a lambda using nothing external can be reused, while one that captures data creates an object and can hold things in memory.

for a middle

Knows that using a field or instance method captures this, keeping the enclosing object alive, and that this can cause leaks.

for a senior

Articulates the listener/registry leak scenario and the capture-only-the-value mitigation, and knows non-capturing lambdas avoid per-call allocation.

for a principal

Explains the invokedynamic/LambdaMetafactory implementation, the singleton optimization, escape-analysis caveats, and sets team conventions for hot-path allocation and listener lifecycle management.

## How lambdas are actually implemented Java lambdas are **not** compiled to anonymous-class `.class` files at build time. The compiler emits an **`invokedynamic`** instruction plus a hidden method holding the lambda body. At first execution, a **bootstrap method** (`LambdaMetafactory`) spins up an implementation of the target functional interface and links the call site. This indirection lets the runtime choose the best strategy. ## Capturing vs non-capturing — the optimization - A **non-capturing** lambda (`() -> 42`, `String::trim` as an unbound method ref) closes over nothing. `LambdaMetafactory` can create **one stateless singleton** and return that same instance every time the call site runs. So a non-capturing lambda in a hot loop typically costs **no allocation** after the first link. - A **capturing** lambda (`() -> x` for a captured local `x`, or `() -> field`) needs somewhere to store the captured values, so the metafactory generates a class with fields and must **instantiate it with the captured values**. Each distinct evaluation that captures fresh values allocates an object (the JIT's **escape analysis** can sometimes stack-allocate or scalar-replace it if it doesn't escape, but you can't rely on that for lambdas that are stored or passed on). Practical takeaway: in tight loops, a non-capturing lambda or one hoisted out of the loop avoids repeated allocation; capturing inside the loop may allocate per iteration. ## The `this`-capture leak When a lambda references an **instance field** or calls an **instance method** of the enclosing class, it implicitly captures **`this`** (the enclosing object), because the field/method is reached through it. The lambda object therefore holds a strong reference to the enclosing instance. If that lambda is handed to something with a **longer lifetime** than the enclosing object should have — for example: - an `EventBus`/observer registry, - a `static` collection or cache, - a scheduled/async task queue, - a UI/framework listener list, then the enclosing object can never be garbage-collected while the registry holds the lambda. This is the **lapsed-listener / listener leak**, and it transitively pins everything the outer object references. ## Mitigations and design guidance 1. **Capture the minimum.** Instead of `() -> this.config.timeout`, copy the needed value first: `int t = this.config.timeout; () -> t;`. Now the lambda is non-capturing of `this` (it captures only the primitive `t`), so it doesn't pin the outer object. 2. **Prefer non-capturing / static context.** A lambda in a `static` method, or one that uses only its parameters, never captures `this`. 3. **Unregister deterministically.** Keep the listener reference and remove it on teardown; or use weak references in the registry. 4. **Hoist stable lambdas** out of hot loops to avoid per-iteration capture allocation. 5. **Measure, don't guess.** Escape analysis and JIT inlining frequently erase the allocation cost of short-lived non-escaping lambdas; profile before micro-optimizing. ## Anonymous classes Anonymous inner classes have the **same `this`-capture leak** characteristic (they also hold the enclosing instance), but they are always a real allocated object — there's no non-capturing singleton optimization. So lambdas are strictly better on the allocation axis and equal on the leak axis. ## Deriving the answer per level - Junior: 'non-capturing can be reused; capturing makes a new object and can leak.' - Middle: + capturing `this` keeps the outer object alive. - Senior: + the registry/listener leak scenario and capture-the-value mitigation. - Principal: + `invokedynamic`/`LambdaMetafactory` mechanics, escape analysis, and team-level guidance on hot paths and lifecycle management.

  • How can you tell whether a lambda captures `this`?
    If its body uses an unqualified instance field or calls an instance method of the enclosing class (or writes `this`), it captures `this`. If it uses only its parameters, static members, and effectively-final locals of primitive/immutable nature, it does not.
  • Does the JIT's escape analysis make capturing-lambda allocation free?
    Sometimes. If the lambda doesn't escape its creating method, the JIT may scalar-replace/stack-allocate it, erasing the heap allocation. But lambdas that are stored, returned, or passed to other threads escape and will be heap-allocated, so you can't rely on it for those.
  • Are anonymous inner classes better or worse than lambdas here?
    Equal on the `this`-capture leak, worse on allocation: an anonymous class is always a freshly allocated object with no non-capturing-singleton optimization, whereas a non-capturing lambda can be a reused singleton.

A non-capturing lambda is a public bus everyone shares (one vehicle, reused). A capturing lambda is a taxi loaded with your luggage — a new car each trip. And if the taxi (lambda) is parked forever in a long-term garage (a registry) with your house keys (this) inside, nobody can ever demolish your house (GC the outer object).

saying these in an interview costs you the question

  • Claiming every lambda is a brand-new object — non-capturing lambdas can be a reused singleton under invokedynamic.
  • Believing lambdas are compiled to anonymous-class files at compile time — they use invokedynamic/LambdaMetafactory at runtime.
  • Thinking capturing a field is harmless — it captures the whole enclosing `this` and can leak the outer object.
  • Assuming escape analysis always eliminates capturing-lambda allocation — only for non-escaping lambdas.

context