Skip to content

HotSpot AOT Code vs. Native Image Is an Operational Choice

•
11 min read

The choice changed, but the feature is not finished

For years, a Java service with a hard startup target faced a familiar choice: accept HotSpot warmup, or turn the application into a closed-world native executable. Project Leyden is making that framing obsolete.

Leyden already delivers useful AOT cache foundations. JDK 24 added ahead-of-time class loading and linking. JDK 25 added a simpler cache workflow and cached method profiles. JDK 26 made cached objects usable with any HotSpot garbage collector. These changes pursue Leyden’s broader goals of better startup, faster time to peak performance, and lower footprint, while retaining the JVM execution model described by Project Leyden.

The most consequential step is not production-ready yet. As of September 24, 2026, JEP 544 is a Candidate. It proposes caching selected C1- and C2-compiled machine code during training and making that code available immediately in later runs. Any claim about HotSpot AOT code, rather than the class, object, and profile caching already delivered, is therefore a claim about a proposal.

That status caveat matters. So does evaluating the right question. This is not primarily a benchmark contest between two compilers. It is a choice between two operating models: a trained, adaptive JVM with a reusable head start, and a native executable built under a closed-world assumption.

A head start and a closed world are different execution models

Leyden’s proposed code cache would not turn HotSpot into an AOT-only runtime. The production JVM could execute cached machine code, interpret an uncached method, compile newly hot code, or discard a cached speculation that no longer fits reality. AOT and JIT code can coexist because both are produced by HotSpot’s C1 and C2 compilers. The transition is intended to be invisible to application code.

Native Image starts from a different premise. It performs reachability analysis from application entry points, then packages reachable code and required runtime components into a native executable. Its optimization model assumes that all code is known at build time and that the running program will not load new code. That closed-world assumption is the source of both its deployment advantages and its compatibility constraints.

Mermaid diagram

The distinction changes how failures behave. A missing or unsuitable Leyden cache entry costs optimization opportunity: HotSpot can load, interpret, profile, and compile at run time. Missing Native Image reachability information can remove required behavior from the executable or surface as a build or runtime failure. Conversely, a Native Image executable does not need a production JVM to recover because the build has already made the important inclusion and compilation decisions.

ConcernLeyden-trained HotSpotNative Image
Unit deployedApplication, matching JDK/runtime, and AOT cacheTarget-specific native executable and its required libraries
Knowledge boundaryTraining observations plus production adaptationBuild-time closed world
Missed pathLoads or compiles normally; loses some cache benefitMust be reachable or described by metadata
Optimization after launchContinues profiling and JIT compilationFixed by the built executable
Main advantageJVM compatibility with less startup and warmup workStandalone startup and footprint without a production JVM
Main operational riskCache mismatch, weak training, or drift reduces benefitBuild complexity and incomplete closed-world configuration

Neither model is universally better. The important question is which model matches the service’s semantics, build system, fleet, and rate of change.

Build pipelines and Java semantics are part of the cost

A Leyden cache has a compact visible workflow. With JDK 25’s command ergonomics, an application can be trained and its cache assembled through one command, then launched with that cache:

Terminal window
java -XX:AOTCacheOutput=app.aot -jar app.jar
java -XX:AOTCache=app.aot -jar app.jar

Internally, recording observations and creating the cache remain separate phases. That separation is useful in CI: training can happen on hardware resembling production, while cache assembly runs on a machine with more CPU and memory. The one-command form can require twice the configured heap because its training and creation sub-invocations each use that heap size; constrained builders may need the explicit two-step workflow described in JEP 514.

Native Image performs static analysis and native compilation, which can take minutes depending on the application and builder. Its optimization levels intentionally trade build duration and binary size against runtime performance. Profile-guided optimization adds another loop:

build instrumented executable
-> run representative workload and collect profile
-> rebuild optimized executable with that profile

The documented Native Image PGO workflow warns against deploying the instrumented binary in production because profiling adds overhead. The feature is also not available in Community Edition. Those facts belong in build-platform and licensing decisions, not in a footnote to runtime benchmarks.

Dynamic behavior changes the engineering bill

Leyden preserves normal runtime reflection and requires no source or framework changes for its delivered class-loading and linking cache. It still has boundaries: JEP 483 does not cache classes loaded through user-defined class loaders, for example. Those classes simply load and link at run time.

Native Image must account for dynamic access that static analysis cannot infer. Reflection, JNI, resources, resource bundles, serialization, and some generated classes may require exhaustive reachability metadata. Runtime definition of genuinely new bytecode is generally unavailable; predefined classes offer only a constrained workaround. Some method-handle and invokedynamic patterns also conflict with the closed world, although ordinary javac lambda and string-concatenation cases are supported when targets remain fixed.

Class initialization is another architectural choice, not merely a build flag. Native Image can initialize selected classes at build time to improve startup and performance, but those classes must not capture assumptions tied to production files, open descriptors, running threads, or other build-machine state. Moving initialization earlier transfers work out of startup while transferring responsibility into the build.

Tooling differs too. Native Image does not support JVMTI and bytecode-based tooling in the same way as a JVM process; its compatibility guide points users toward native debuggers and monitoring tools. A team dependent on agents, runtime class transformation, or established JVM diagnostics should price that migration explicitly. Leyden also restricts cache reuse when JVMTI agents alter classes or class loading, but falling back to ordinary HotSpot remains fundamentally different from changing the application’s execution model.

Startup is only one phase, and training data can lie

A single “time to first request” number hides three distinct periods:

  1. Startup: loading configuration, classes, metadata, and initial objects.
  2. Warmup: gathering profiles and compiling hot methods.
  3. Steady state: serving a stable or changing workload after optimization.

Delivered Leyden features move class loading, linking, object creation, and profile collection out of those first two periods. Cached method profiles let C2 compile sooner, but production still pays compilation cost unless suitable compiled code is also cached. Proposed JEP 544 code caching would remove more of that cost by starting selected methods with trained machine code.

That cached code is a head start, not a ceiling. Production profiling continues. If traffic changes, HotSpot can deoptimize stale speculative code and compile newly hot paths. Later JIT code can even exploit run-specific facts unavailable to AOT assembly, such as the current class-initialization state or a static final value established only in that process.

Native Image instead offers immediate behavior determined by its build and optimization profile. That can be exactly right for short-lived functions, command-line tools, aggressive scale-to-zero platforms, or services where a fixed startup envelope matters more than adaptation. It also means that an unrepresentative PGO workload can cause the compiler to optimize the wrong paths and reduce performance.

Leyden training has the same underlying data problem. OpenJDK recommends fully configuring the application and exercising typical production paths, while avoiding broad regression or stress suites that drag rare behavior into the cache. Mock databases and networks can pollute a cache with test-only classes. Skipping those interactions is often safer: production-only paths then load normally at run time.

The recovery story differs, but bad training is not free in either system:

Training problemLeyden consequenceNative Image PGO consequence
Common production path omittedLess cached work; HotSpot profiles and compiles onlinePath exists if reachable, but may be poorly optimized
Rare path overrepresentedLarger or less useful cacheOptimization budget can favor the wrong path
Traffic mix changesOnline profiling and JIT can adaptRebuild required to incorporate a better profile
Test doubles load different codeCache pollution; real code loads at runtimeProfile and possibly reachability configuration diverge from production

Treat profiles like operational artifacts. Version them against application inputs, record the training scenario, track workload drift, and define retraining triggers. Useful triggers include a dependency or JDK update, a material endpoint-mix shift, a new tenant shape, a collector change, or a deployment to different CPUs.

Published proposal numbers are useful for feasibility, not selection. JEP 544 reports roughly 50–70% startup reductions without cached code and 65–80% with it across five frameworks on one two-core Linux/x64 setup. JEP 483 reports a 42% improvement for both a small Stream example and Spring PetClinic, but their caches were 11.4 MB and 130 MB respectively. These measurements show that Leyden can move startup materially. They do not compare your service with Native Image, and they show that benefit and cache size are workload-specific.

Portability means deciding where specialization belongs

Java bytecode is portable; trained machine artifacts are not universally portable. A Leyden cache requires close agreement between training and production: the same JDK release, operating system and architecture, a consistent class path or module graph, and compatible agent behavior. If a base cache is unusable, HotSpot normally warns and continues without it. A required-cache mode can turn that condition into launch failure, which is preferable when CI is meant to validate the artifact rather than silently benchmark a fallback.

Proposed cached machine code adds tighter boundaries. JEP 544 does not target cross-compilation. The production machine needs the architecture and CPU features used to create the code, initially x64 or AArch64, and it must use the same garbage collector because generated code embeds collector-specific barriers. When only the code is incompatible, HotSpot may still reuse compatible cached classes and profiles and regenerate machine code through JIT.

Do not confuse this with JDK 26’s GC-neutral object cache. JEP 516 introduced a streamable format that lets cached Java objects work with any HotSpot collector. It does not make JEP 544’s proposed machine code collector-neutral. The streamable object format can also hide cold filesystem reads on a background thread, but benefits from an extra CPU core; a mapped, collector-specific form may suit constrained machines with warm storage better.

Native Image is target-specific too. Its command-line options let a builder select a target OS and architecture and choose static or mostly static linkage, but each resulting binary still needs to be built and tested for its deployment target. CPU tuning makes the tradeoff explicit: -march=native specializes for the build machine, while -march=compatibility uses a more conservative instruction set for a broader fleet.

For either approach, a multi-architecture or heterogeneous fleet creates an artifact matrix. The difference is where fallback happens. HotSpot can reject incompatible cached code and continue adaptively, potentially with a startup regression. A native executable must already match its target. Artifact count, image size, container pull time, cache size, and target-specific test coverage therefore belong in the same scorecard as runtime latency.

Practical takeaway: choose the operating model first

Prefer Native Image when the dominant requirement is a standalone native artifact with minimal startup and footprint, the application fits a controlled closed world, and the organization can own reachability metadata, target builds, and native-oriented diagnostics. It is especially compelling when processes are short-lived enough that adaptive optimization has little time to pay back.

Prefer Leyden’s HotSpot model when dynamic Java semantics, JVM tooling, adaptive peak performance, or workload drift matter more. Its delivered caches already reduce startup and warmup work. If JEP 544 is delivered, cached HotSpot code will narrow the remaining gap without imposing an AOT-only performance ceiling. Until then, evaluate code caching as a proposed capability, not a production fact.

For a serious decision, build both candidates around the real service and deployment topology. Measure:

  • training and total build duration;
  • peak build memory and CI capacity;
  • cache, executable, and container-image size;
  • registry transfer and cold-node pull time;
  • startup latency as a distribution, not a best run;
  • time to steady-state throughput and latency;
  • steady-state behavior under realistic traffic shifts;
  • observability and incident-response workflows;
  • artifact count across OS, architecture, CPU, and collector choices;
  • profile drift, cache rejection, and rebuild frequency.

Then run failure tests: omit a trained route, change the traffic mix, move to an older CPU, switch collectors, attach required tooling, and update a framework dependency. The better choice is the one whose degraded mode your team can operate—not the one that wins a clean-room startup chart.

References

  1. JEP 544: Ahead-of-Time Code Compilation
  2. JEP 515: Ahead-of-Time Method Profiling
  3. JEP 514: Ahead-of-Time Command-Line Ergonomics
  4. JEP 483: Ahead-of-Time Class Loading & Linking
  5. JEP 516: Ahead-of-Time Object Caching with Any GC
  6. Project Leyden
  7. Native Image Compatibility Guide
  8. Native Image Reachability Metadata
  9. Native Image Profile-Guided Optimization
  10. Native Image Build Overview
  11. Native Image Optimizations and Performance
  12. Native Image Command-line Options

Written by

Farshad Akbari

Software engineer writing about Java, Kotlin TypeScript, Python, data systems and AI

Keyboard Shortcuts

Navigation

  • Open search ⌘K
  • Next article j
  • Previous article k

Actions

  • Toggle dark mode d
  • Toggle table of contents t
  • Show this help ?
  • Close modal Esc

Shortcuts are disabled when typing in inputs or textareas.