What does it mean for a JIT compiler to compile a method speculatively, and what does the JVM do at runtime when one of those speculative assumptions turns out to be wrong?
answer
- profile → assumption, not proof
- uncommon trap = stub back into the runtime
- deopt = rebuild interpreter frames, resume at same bci
- not entrant → reprofile → recompile without the assumption
- trap limits stop the speculate/deopt loop
basics
~20 sThe JIT compiles using the profile gathered so far — e.g. only one receiver type seen, this branch never taken — and replaces the unproven path with an uncommon trap. If the assumption breaks, the trap fires, the runtime deoptimizes that frame back to the interpreter at the same bytecode, and the method is later recompiled without the failed assumption.
solid answer
~60 sWhile code runs interpreted (and in the profiling compiler tier) the JVM records a profile: which receiver types a call site actually saw, which branches were taken, whether a null or an exception ever occurred, and what the loaded class hierarchy looks like. The optimizing compiler treats that profile as an **assumption** rather than a proof — it can then inline a virtual call as if it were static, drop a branch entirely, or specialize on a type. Where the assumption might be violated, the compiler emits an **uncommon trap** instead of real code: a small stub that jumps into the runtime. Hitting it triggers **deoptimization** — the runtime rebuilds interpreter frames from the compiled frame using recorded debug information and resumes interpreting at the exact bytecode index, so semantics are preserved. The compiled method is then typically marked *not entrant* (no new calls enter it), the method reprofiles, and it may be recompiled without the assumption that failed. After repeated traps for the same reason, HotSpot stops speculating there and compiles the general case. Deoptimization is the safety net that makes aggressive optimization legal, not a bug.
code
text · 9 lines$ java -XX:+PrintCompilation Main
432 45 % 3 Main::process @ 12 (58 bytes)
455 47 4 Main::process (58 bytes)
1120 47 4 Main::process (58 bytes) made not entrant
1121 52 3 Main::process (58 bytes)
1189 55 4 Main::process (58 bytes)
# 'made not entrant' = the C2 version was invalidated after a trap;
# the method reprofiles at tier 3 and is recompiled at tier 4.go deeper
Know the story in three beats: the JIT bets on the observed profile, a guard fails, execution drops back to the interpreter and the method is recompiled.
Name concrete speculations (monomorphic inlining, pruned branches, implicit null checks), the uncommon trap, and the not-entrant/reprofile/recompile cycle.
Add reasons and actions, the trap counters that withdraw speculation, and the observable signals — PrintCompilation 'made not entrant', JFR deoptimization events, and latency blips on first rare input.
Frame it as the core bargain of an adaptive runtime: peak throughput from profile-based facts, paid for with warmup, memory for profiles and debug info, and tail-latency risk that must be managed at the architecture level.
## Why speculate at all Java is dynamically dispatched and dynamically loaded. Statically, almost every `interface` call could go anywhere, any class could be subclassed later, and any branch could be taken. A compiler restricted to what it can *prove* would inline almost nothing, and inlining is the enabling optimization — without it there is no cross-method constant folding, escape analysis, or loop optimization. So HotSpot's optimizing compiler compiles against what the program has actually *done*, not what it might do. That is legal only because the JVM keeps a way out. ## The profile Every method carries a MethodData structure filled in while the method runs interpreted and in the profiling compilation tier. It records, per bytecode: - **receiver types** at virtual/interface call sites, with counts ("98% ArrayList, 2% LinkedList", or "only ArrayList ever"); - **branch counts** — how often each side of an `if` was taken, including "never"; - **null seen / exception seen** flags for checks and casts; - array store types, `instanceof` outcomes, and loop trip counts. Alongside the profile, the compiler consults global facts such as class hierarchy analysis ("only one implementation of this interface is loaded") and "no subclass overrides this method yet". ## Turning a profile into an assumption Typical speculations: - **Monomorphic inlining.** One receiver type seen → inline that implementation behind a type guard, or devirtualize outright based on the hierarchy. - **Branch pruning.** A branch never taken → do not compile it at all; emit an uncommon trap in its place. - **Implicit null checks.** Nulls never seen → omit the explicit test and let the hardware fault handler turn the segfault into the trap. - **Effectively-final types and constants.** A field that has only ever held one class, or a stable value, can be treated as constant behind a guard. What you get in the machine code is fast straight-line code plus tiny guards; the *cold* alternative is not compiled at all, which is why compiled code can be dramatically smaller and faster than a general implementation. ## The uncommon trap and deoptimization When a guard fails or an uncompiled path is reached, control enters the **uncommon trap** stub with a recorded *reason* (`class_check`, `null_check`, `unstable_if`, `unreached`, `array_check`, …) and an *action* (`none`, `maybe_recompile`, `reinterpret`, `make_not_entrant`). The runtime then **deoptimizes**: it reconstructs one or more interpreter frames from the single compiled frame (which may represent several inlined methods) using debug information the compiler emitted alongside the code, restores locals and the expression stack, and continues execution in the interpreter at the exact bytecode index where the trap sits. Program semantics are unchanged — an observer only sees a slower few microseconds. Afterwards, depending on the action: - the compiled nmethod may be made **not entrant** so no new invocation uses it (existing frames may keep running or be deoptimized on stack); - the method reprofiles in the interpreter/profiling tier; - it is queued for **recompilation**, now knowing the previously unseen case — for example compiling a bimorphic call or a real branch instead of a trap. Per-method and per-bytecode trap counters (governed by `PerMethodTrapLimit`/`PerBytecodeTrapLimit`) mean this cannot loop forever: after a few traps at the same place, the compiler stops speculating there and emits the general, slower-but-stable code. ## What this looks like from outside - A method runs fast for minutes, then a rare input arrives and the next few calls are slow, after which it is fast again — one trap, a reprofile and a recompile. - `-XX:+PrintCompilation` shows entries like `made not entrant` and repeated compilations of the same method at the same or higher level. - JFR emits `jdk.Deoptimization` events carrying the method, bytecode index and reason. ## Why this is a feature Deoptimization is what lets a managed runtime beat an ahead-of-time compiler on dynamically dispatched code: the JIT gets to compile *the program that is actually running*, including facts (only one implementation loaded, this branch never happens, this field is effectively constant) that no static compiler could rely on. The price is warmup, some memory for profiles and debug info, and occasional latency blips when speculation fails.
- If deoptimization can always fall back, why does the JVM ever stop speculating at a site?Because falling back is expensive: each trap costs a transition into the runtime, frame reconstruction, interpretation and usually a recompilation. HotSpot keeps per-method and per-bytecode trap counters, and once a site has trapped a few times for the same reason it compiles the general case instead, trading peak speed for stability.
- Does deoptimization change program behaviour or observable state?No. The runtime reconstructs the interpreter frames — locals, expression stack, and the exact bytecode index — from debug information recorded by the compiler, so execution resumes as if the method had been interpreted all along. Only timing changes.
- What is the difference between the compiled code being 'made not entrant' and being discarded?Not entrant means no new invocations may enter that compiled version, while frames already executing it may continue. Once no frame is using it and it is unreachable, it becomes a zombie and its code-cache space is eventually reclaimed.
A commuter who has never seen the bridge closed stops checking traffic reports and drives straight there. If it ever is closed, they stop, go back to the map, and from then on plan a route that accounts for closures — the shortcut was only safe because a fallback existed.
saying these in an interview costs you the question
- Describing deoptimization as an error or a JIT bug rather than the mechanism that makes speculation legal
- Claiming the JIT only applies optimizations it can prove statically
- Thinking a deoptimized method is permanently interpreted afterwards
- Believing deopt can loop forever, unaware of the per-site trap limits that withdraw speculation
- Confusing deoptimization with on-stack replacement — OSR moves execution into compiled code, deopt moves it out