skip to content

Explain the three escape states (NoEscape, ArgEscape, GlobalEscape) and give an example of code that produces each.

level: seniorimportance: should knowfreq 35%

answer

  1. NoEscape: stays in method → all optimizations
  2. ArgEscape: passed to callee only → depends on inlining
  3. GlobalEscape: returned/stored/thrown/published → no optimization
  4. Worst-of state wins; analysis is conservative
  5. Lattice: NoEscape ⊏ ArgEscape ⊏ GlobalEscape

basics

~20 s

NoEscape means the object stays inside the method. ArgEscape means it is only handed to another method it calls. GlobalEscape means it leaks out — for example by being returned or stored in a field — so the JVM can no longer optimize away its allocation.

solid answer

~50 s

The JIT classifies each new object into one of three escape states. NoEscape: the reference never leaves the creating method and no other thread can see it — a temporary you only read locally. This unlocks scalar replacement, stack allocation, and lock elision. ArgEscape: the object is passed into a callee as an argument but is not stored globally or returned; it "escapes" only into that callee's frame. If that callee is inlined, the compiler can often still scalar-replace it; if not, it conservatively treats the object as escaping. GlobalEscape: the reference becomes reachable from outside the method — it is returned to the caller, stored in a static or instance field, thrown, or published to another thread. No allocation-avoiding optimization is possible. The states form a lattice from best (NoEscape) to worst (GlobalEscape), and the analysis is conservative: if it cannot prove a tighter state, it assumes GlobalEscape.

go deeper

for a junior

Can say objects that stay inside a method are cheap and objects that leave it are not, even without naming the three states.

for a middle

Names the three states and gives a returned-object example of GlobalEscape.

for a senior

Explains all three with examples, ties ArgEscape's outcome to inlining, and notes the worst-of/conservative rule.

for a principal

Reasons about how the lattice interacts with the inlining budget and devirtualization, and can predict how a specific refactor flips an object's state and its allocation cost.

## Background you need first **Escape analysis** is a JIT-compiler analysis that decides whether an object created inside a method can be observed from outside that method or by another thread. "Escape" = a reference leaks beyond the current scope. The result is encoded as one of three **escape states**, which form an ordered lattice from most-optimizable to least. ## NoEscape — the object stays local The object's reference never leaves the method and is never published to another thread. The compiler can prove the object's entire lifetime is contained in this one method invocation. This is the state that unlocks the strong optimizations: - **Scalar replacement** (explode the object into register/stack-held fields, so it is never allocated), - **stack allocation**, and - **lock elision** (remove `synchronized` on an object only one thread can see). ```java int distanceSquared(int ax, int ay, int bx, int by) { Point a = new Point(ax, ay); // NoEscape Point b = new Point(bx, by); // NoEscape int dx = a.x - b.x; int dy = a.y - b.y; return dx * dx + dy * dy; // we return an int, not the Points } ``` Both `Point`s are read-only locals; the JIT can replace them with four `int` locals and allocate nothing. ## ArgEscape — the object escapes into a callee The object is passed as an **argument** to another method (a callee) but is not stored in any global location and is not returned up to the caller. It "escapes" the current method only in the sense that the callee's frame now sees it. ```java int perimeter(int ax, int ay, int bx, int by) { Point a = new Point(ax, ay); // ArgEscape: passed to length() Point b = new Point(bx, by); return length(a) + length(b); // length doesn't store or return a/b } ``` What happens next depends on **inlining**: if `length()` is inlined into `perimeter()`, the analysis sees the full combined body, can confirm the points never truly escape, and may scalar-replace them. If `length()` is *not* inlined (too big, or a virtual call the compiler cannot resolve to a single target), the callee is opaque — the compiler must assume the worst about what `length()` does with its argument, and the object is treated as escaping. ## GlobalEscape — the object leaks out The reference becomes reachable from outside the creating method. Once this happens, no allocation-avoiding optimization is legal, because external code (or another thread) may observe or mutate the object, so it must genuinely exist on the heap. Things that cause GlobalEscape: - **Returning** the object: `return new Point(x, y);` - **Storing into a static field**: `LAST = new Point(x, y);` - **Storing into another object's instance field**: `this.origin = new Point(0,0);` - **Throwing** it as an exception. - **Publishing to another thread** (e.g. assigning into a shared structure, a static, or handing it to a started thread/executor). ```java Point makeOrigin() { Point p = new Point(0, 0); // GlobalEscape: escapes via return return p; } ``` ## The lattice and conservatism The states are ordered NoEscape ⊏ ArgEscape ⊏ GlobalEscape. When a single object hits multiple uses, its state is the **worst** (most escaping) of them. And the analysis is **conservative by design**: if it cannot prove a tighter state — for instance because a call is not inlined, or control flow is too complex — it falls back to GlobalEscape and skips optimization. The correctness rule is "never optimize away an object someone might observe," so when in doubt it does nothing. ## Why this matters in practice Knowing the states explains *why* a tiny refactor can change performance: turning a local temporary into a returned value, caching it in a field, or breaking a hot method so a callee no longer inlines can flip an object from NoEscape (free) to GlobalEscape (allocated + GC'd).

  • If an object is ArgEscape, what single compiler decision most determines whether it still gets optimized?
    Inlining. If the callee is inlined, the compiler sees the whole body and can often prove NoEscape and scalar-replace; if not, the callee is opaque and the object is treated as escaping.
  • An object is read locally but also stored once into a static field. What is its escape state?
    GlobalEscape — the state is the worst of all its uses, and storing into a static field publishes it globally.

saying these in an interview costs you the question

  • Saying ArgEscape always defeats the optimization — if the callee inlines, the object can still be scalar-replaced
  • Treating the three states as unordered categories rather than a worst-case lattice
  • Thinking a local variable alone guarantees NoEscape regardless of how it is used
  • Forgetting that publishing to another thread is also an escape

context