skip to content

Native images have no JIT compiler. What are the practical performance consequences versus a warmed-up JVM?

level: seniorimportance: should knowfreq 34%

answer

  1. AOT only, no interpreter/JIT for app code
  2. wins: startup + RSS + predictability
  3. loses: sustained peak throughput
  4. JIT = profile-guided + deopt safety net
  5. PGO: instrument -> profile -> rebuild

basics

~20 s

Native 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 s

The 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

for a junior

Know that native starts fast and uses less memory but has no JIT.

for a middle

Contrast AOT vs JIT and name startup/memory wins and throughput trade-off.

for a senior

Explain profile-guided speculation, deoptimization, and PGO as the mitigation; frame the workload decision.

for a principal

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

context