Native images have no JIT compiler. What are the practical performance consequences versus a warmed-up JVM?
answer
- AOT only, no interpreter/JIT for app code
- wins: startup + RSS + predictability
- loses: sustained peak throughput
- JIT = profile-guided + deopt safety net
- PGO: instrument -> profile -> rebuild
basics
~20 sNative images start instantly and use less memory because everything is compiled ahead of time. But without a JIT there is no runtime profiling, so long-running peak throughput can be lower than a warmed-up JVM.
solid answer
~50 sThe closed-world model produces a fully AOT-compiled executable with no interpreter for your code and no JIT. Practically that means near-instant startup (no class loading, verification, or JIT warm-up), immediate peak-ish performance from the first request, and a much smaller memory footprint since there is no profiling data, no compiled-code caches beyond what's baked in, and dead code is removed. The trade-off is throughput: a HotSpot JVM's JIT observes actual runtime behavior and applies profile-guided, speculative optimizations (inlining hot paths, branch prediction, deoptimization) that AOT cannot match for long-running, hot workloads. GraalVM mitigates this with Profile-Guided Optimization (PGO): you run an instrumented build, collect a profile, and feed it back into the final native compile to recover much of the gap. So: native wins on startup, memory, and density; a warmed JVM can still win on sustained max throughput.
go deeper
Know that native starts fast and uses less memory but has no JIT.
Contrast AOT vs JIT and name startup/memory wins and throughput trade-off.
Explain profile-guided speculation, deoptimization, and PGO as the mitigation; frame the workload decision.
Drive the org decision: which services go native, benchmark methodology (warm JVM baseline), PGO/GC tuning, dev-loop cost.
## Why there is no JIT The closed-world assumption fixes the reachable set at build time and **AOT-compiles** it to machine code. There is therefore **no bytecode interpreter for application code and no Just-In-Time compiler** at runtime. On a normal **HotSpot JVM** the lifecycle is: interpret bytecode → profile → JIT-compile hot methods (C1 then C2/Graal) → possibly **deoptimize** when a speculative assumption breaks. Native image removes that entire machinery. ## What you gain - **Startup latency**: milliseconds, because there is no class loading, no bytecode verification, and no warm-up phase. Ideal for **serverless / scale-to-zero / CLI**. - **Memory footprint (RSS)**: substantially lower — no JIT compiler threads, no profiling counters, no code cache growth, and dead-code elimination shrinks the working set. - **Predictability**: performance is flat from the first request; no warm-up cliff, no deopt storms. - **Density**: more instances per host; faster autoscaling. ## What you give up - **Peak throughput on hot code**: the JIT's **profile-guided, speculative** optimizations (aggressive inlining of actually-hot paths, monomorphic call-site specialization, escape analysis informed by real behavior, deoptimization safety net) can beat static AOT for **long-running** compute-heavy services. AOT must be conservative because it optimizes for all possible executions, not the observed one. ## Closing the gap: PGO GraalVM (Oracle GraalVM / native-image) supports **Profile-Guided Optimization**: 1. Build an **instrumented** image (`--pgo-instrument`). 2. Run it under representative load to produce an `.iprof` profile. 3. Rebuild feeding the profile (`--pgo=...`). The final image now has realistic hot-path data and typically recovers a large fraction of the throughput difference. Also relevant: choosing the right **GC** for native (e.g. the Serial GC default is tiny-footprint; G1 is available in Oracle GraalVM for throughput). ## Decision framing | Workload | Prefer | |---|---| | Serverless / functions, CLI, scale-to-zero | Native image | | High instance density / fast autoscale | Native image | | Long-running CPU-bound max-throughput service | Warmed JVM (or native + PGO) | | Heavy runtime dynamism / plugins | JVM | ## Spring-specific notes - Spring Boot's `nativeCompile` (via `org.graalvm.buildtools.native`) and the AOT phase make the app closed-world-ready; there's no Spring switch that re-enables a JIT. - **Build time is long** and native tests are slow, so most teams run the JVM in the inner dev loop and produce native images in CI for the artifacts that benefit. ## Gotchas - Don't benchmark native vs JVM using **cold JVM** numbers — that flatters native on throughput; compare against a **warmed** JVM. - PGO profiles must be **representative**; a bad profile can mis-optimize. - Native peak memory can spike during GC depending on collector choice; measure RSS under load, not just at idle.
- How does Profile-Guided Optimization recover throughput without a JIT?You build an instrumented image, run it under representative load to capture a profile, then rebuild feeding that profile so AOT can inline/optimize the actually-hot paths — moving profiling from runtime to build time.
- Why can a warmed HotSpot JVM still out-throughput a plain native image on a long-running service?The JIT makes speculative, profile-driven optimizations at runtime (aggressive inlining, monomorphic specialization) with a deoptimization safety net; static AOT must be conservative across all executions.
saying these in an interview costs you the question
- Claiming native image is always faster than the JVM in every dimension
- Comparing native against a cold, un-warmed JVM to prove throughput superiority
- Believing PGO reintroduces a runtime JIT (it doesn't)
- Ignoring that native build/test time is a real dev-loop cost