Escape Analysis Internals
Purpose
Explain why a specific object was not eliminated, using the mechanism rather than folklore. The failure this skill prevents is chasing the wrong target: tuning bytecode escape-analysis limits toward ArgEscape in the hope of removing an allocation. In current C2, ArgEscape is not eligible for scalar replacement; it can make monitor elimination possible, but even that remains subject to the surrounding lock shape and compiler policy.
Escape classification is the fixed point of edge propagation over a connection graph, and
macro expansion then removes an AllocateNode or lowers the surviving allocation. Dead-code and
macro cleanup can also discard an allocation whose result has no surviving use, independently of
EA. That is one reason a zero-allocation microbenchmark needs an EA-disabled control and emitted-
code/compiler-log evidence.
Workflow
- Confirm the baseline is not the explanation. Record vendor, full JDK build, compiler,
tiering and effective flags; this material targets HotSpot C2, chiefly JDK 25. Check
DoEscapeAnalysis,EliminateAllocationsandEliminateLockswith unlocked-XX:+PrintFlagsFinal; inspectReduceAllocationMergesonly where available (JDK 22+). Preserve the project's target rather than upgrading it. Check diagnostics against the exact VM. - Establish that allocation is really happening, then that EA is the mechanism.
gc.alloc.rate.normat the object's full size is the reason to continue. At zero, rerun with-XX:-DoEscapeAnalysis: still zero means EA dependence was not demonstrated. Inspect dead-code removal, caching, untaken paths and measurement scope before attributing a mechanism. - Ask the compiler before theorising. On the target HotSpot product build,
-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilationwrites<eliminate_allocation>and<eliminate_lock>in C2 tasks. Join compile IDs to installed C2 nmethods and match the allocation's method/BCI and inline context; absence alone is inconclusive. Seereferences/diagnosing-elimination.md. - Find the inlining boundary.
-XX:+PrintInlining, tier-4 tree — is there a refusal on the chain that carries the object? Confirm the callee's real bytecode size withjavap -c -p, never by eyeballing the source. - Decide which state is achievable, then set the expectation accordingly. A callee that
fits within
MaxBCEAEstimateSizeand receives a successful non-escaping BCEA summary can leave its argument ArgEscape — that can enable lock elision, not scalar replacement across that call. NoEscape is necessary for scalar replacement, but not sufficient: identity-sensitive uses, array/field limits, unsafe access, merges, and other graph shapes can still preserve the allocation. - If everything was inlined, match the shape against the "why did this allocation
survive" table — a merge with an unsupported user, an identity hash, a non-constant array
index, a field or array limit, a taken rare branch — then trace the escaping edge to its
sink. See
references/connection-graph.md. - Fix the cause, not the symptom. Construct the object inside the rare branch, split the
escaping path into its own method, reduce polymorphism, or raise
MaxBCEAEstimateSize— the last only when the gain sought is lock elision. Then repeat steps 2 and 3 on the same load. - Investigate rematerialisation cost separately, with
-Xlog:deoptimization=debugorjdk.Deoptimizationover a real window, not with JMH.
Rules
- ArgEscape is not eligible for scalar replacement in the examined C2 implementation. An object argument to an ordinary non-inlined Java call is at least ArgEscape; inlining can remove that boundary. Intrinsics and specially modelled runtime operations need their own graph analysis; do not apply the ordinary-call rule to every source-level invocation.
- The analysis is flow-insensitive over the compiled graph. A branch that stores the object marks it for every path when that store is present in the compiled graph. Profiles may instead lead C2 to replace a sufficiently unlikely branch with an uncommon trap; “taken once” versus “never” is not the portable decision boundary. Inspect the graph/log and trap history. Constructing the object inside the escaping branch often restores the common path, but validate changed allocation, code size, and deoptimization behavior.
- Merges are no longer categorically rejected. JDK 22 added reduction for supported Phis over allocations (JDK-8287061); JDK 23 added nullable cases (JDK-8316991). Eligible user shapes are narrow—principally supported field loads, safepoint debug use, constant/null comparisons, and guarded casts. Calls, stores, class/identity-sensitive access, arrays, or other unsupported uses can still block reduction; confirm against the target release and log.
MaxBCEAEstimateSize(default 150) measures bytecode bytes of the non-inlined callee, not object size. Raising it extends the summary to larger callees without inlining them; its ceiling is ArgEscape, so the real benefit is lock elision across the inlining boundary.- No EA flag measures "object size" in the usual sense.
EliminateAllocationArraySizeLimit(64) counts array elements andEliminateAllocationFieldsLimit(512, diagnostic) counts fields; both are refusals to hold that many scalars live at every safepoint, not a bug. PrintEscapeAnalysis,PrintEliminateAllocationsandPrintEliminateLocksaredevelopflags: a product JVM refuses to start on them, andPrintEscapeAnalysisis not aCompileCommandoption on the examined build — bothoption,C::m,PrintEscapeAnalysisandPrintEscapeAnalysis,C::mreportUnrecognized option. On Temurin 25.0.3 both exited 1 before-versionran; check exit status and actual execution on the target VM. There is no per-method form even on a debug build.LogCompilationis the product-build substitute.- For allocation evidence in production use
jdk.ObjectAllocationSample.jdk.ObjectAllocationInNewTLABandjdk.ObjectAllocationOutsideTLABareenabled=falsein bothdefault.jfcandprofile.jfcon JDK 25 (JDK-8257602), so a zero count from them proves nothing unless the event was enabled by name. EvenObjectAllocationSampleis throttled sampling — in the lab, JMH-prof gcremains the primary metric. - Scalar replacement is not stack allocation. C2 decomposes the object into scalars; Graal's partial EA decides when to materialise on the heap, per path. Do not describe either as "stack allocation", and do not describe Graal's partial escape analysis as a more sophisticated version of C2's — it is a different technique, run iteratively interleaved with inlining rather than after parse-time inlining settles.
- Rematerialisation cost per event grows with the number and shape of virtual objects live at
the deoptimizing safepoint; total cost also grows with deoptimization rate. Count allocation,
field restoration, frame reconstruction, and downstream GC alongside recompilation.
-XX:+TraceDeoptimizationcan expose objects on a controlled test run when supported. - Not every call boundary is worth attacking. If the callee retains the object in escaping state, inlining cannot erase that semantic escape. Distinguish actual retention from an ArgEscape argument whose callee only reads fields; assess code size and workload benefit before trying to inline the latter.
- Project Valhalla may reduce identity and flattening costs structurally, but do not design from an EA draft as if it were a shipped guarantee. Recheck JEP status and the deployed release; JDK 17, 21, and 25 code still depends on existing object and EA behavior.
- Label any speedup figure taken from a composite or third-party case as such. Measure
gc.alloc.rate.normbefore and after on the same load rather than inferring it.
Deliver the allocation site/compile ID, observed allocation rate, supported mechanism or remaining hypothesis, and the smallest confirming check. Missing compiler/runtime evidence means a conditional diagnosis, not a flag recommendation.
References
- The connection graph — node and edge kinds, the propagation path from parse to escape state with the iterative loop and its bailout, what BCEA can and cannot buy, reducible merges and their exact user rules, the three exits from macro expansion including the unused-allocation yank, boxing and string concatenation, lock elision kinds, the rematerialisation mechanism, and how Graal's per-path materialisation differs. Read when explaining why a specific object received the state it did.
- Diagnosing a failed elimination — the procedure
with its EA-off control, the "why did this allocation survive" table with measured
results on 25.0.3, the flag table by class, what
CompileCommanddoes not accept, readingLogCompilation'seliminate_allocation/eliminate_lockelements, the corrected JFR event matrix, lock elision timings, and the checklists. Read while running an investigation. - HotSpot C2 escape analysis source, JDK 25
- JDK-8287061: allocation-merge rematerialization
- Project Valhalla status