JVM Advent

The JVM Programming Advent Calendar

Run Into the New Year with Java’s Ahead-of-Time Cache Features

As the year comes to a close, turn your focus to boosting your Java application performance by applying Ahead-of-Time (AOT) cache features in recent JDK releases. This article guides you through using AOT cache optimizations in your application, thereby minimizing startup time and achieving faster peak performance.

What is the Ahead-of-time cache in the jdk

JDK 24 introduced the Ahead-Of-Time (AOT) cache, a HotSpot JVM feature that stores classes after they are read, parsed, loaded, and linked. Creating an AOT cache is specific to an application, and you can reuse it in subsequent runs of that application to improve the time to the first functional unit of work (startup time).

To generate an AOT cache, you need to perform two steps:

  1. Training by recording observations of the application in action. You can trigger a recording by setting an argument for the -XX:AOTMode option and giving a destination for the configuration file via -XX:AOTConfiguration:
    java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf 
         -cp app.jar com.example.App ...

    This step aims to answer questions like “Which classes does the application load and initialize?”, “Which methods become hot?” and store the results in a configuration file (app.aotconf).

  2. Assembly that converts the observations from the configuration file into an AOT cache (app.aot).
    java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf 
         -XX:AOTCache=app.aot -cp app.jar

To benefit from a better startup time, run the application by pointing the -XX:AOTCache flag to the resulting AOT cache.

java -XX:AOTCache=app.aot -cp app.jar com.example.App ...

The improved startup time is the result of shifting work, usually done just-in-time when the program runs, earlier to the second step, which creates the cache. Thereafter, the program starts up faster in the third phase because its classes are available from the cache immediately.

The three-step workflow (train+assemble+run) became available starting with JDK 24, via JEP 483: Ahead-of-Time Class Loading & Linking, the first feature merged from the research done by Project Leyden. A set of benchmarks prove the effectiveness of this feature and other Leyden performance-related ones, as displayed by Figure 1.

Figure 1: AOT Cache Benchmarks as of JDK 24

Figure 1: AOT Cache Benchmarks as of JDK 24

In JDK 25, the changes in JEP 515 – Ahead-of-Time Method Profiling enabled frequently executed method profiles to be part of the AOT cache. This addition improves application warm up by allowing the JIT to start generating native code immediately at application startup. The new AOT feature does not require you to add more constraints to your application execution; just use the existing AOT cache creation commands. Moreover, benchmarks showed improved startup time too (Figure 2).

Figure 2: AOT Cache Benchmarks as of JDK 25

Figure 2: AOT Cache Benchmarks as of JDK 25

JDK 25 also simplified the process for generating an AOT cache by making it possible to do it in a single step, through setting the argument for -XX:AOTCacheOutput flag:

# Training Run + Assembly Phase
java -XX:AOTCacheOutput=app.aot \
     -cp app.jar com.example.App ...

Upon passing -XX:AOTCacheOutput=[cache location], the JVM creates the cache on its shutdown. JEP 514 – Ahead-of-Time Command-Line Ergonomics introduced the two-step process for creating and using the AOT cache.

# Training Run + Assembly Phase
java -XX:AOTCacheOutput=app.aot \
     -cp app.jar com.example.App ...

# Deployment Run
java -XX:AOTCache=app.aot -cp app.jar com.example.App ...

The two-step workflow may not work as expected in resource-constrained environments. The sub-invocation that creates the AOT cache uses its own Java heap with the same size as the heap used for the training run. As a result, the memory needed to complete the one-step AOT cache generation is double the heap size specified on the command line. For example, if the one-step workflow java -XX:AOTCacheOutput=... is accompanied by -Xms2g -Xmx2g, specifying a 2GB heap, then the environment needs 4GB to complete the workflow.

A division of steps, as in a three-phase workflow, may be a better choice if you intend to deploy an application to small cloud tenancies. In such cases, you could run the training on a small instance while creating the AOT cache on a larger one. That way, the training run reflects the deployment environment, while the AOT cache creation can leverage the additional CPU cores and memory of the large instance.

Regardless of which workflow you choose, let’s take a closer look at AOT cache requirements and how to set it up to serve your application needs best.

How to Craft the AOT Cache Your Application Needs

Training and production runs should produce consistent results, just faster in deployment runs. To achieve that, the assembly phase intermediates what happens between training and production runs (Figure 3).

Figure 3: Training / Assembly / Deployment

Figure 3: Training / Assembly / Deployment

For consistent training runs and the subsequent ones, make sure that:

  • Your JARs preserve their timestamp across training runs.
  • Your training runs and the production one use the same JDK release for the same hardware architecture and operating system.
  • Provide the classpath for your application as a list of JARs, without any directories, wildcards or nested JARs.
  • Production run classpath must be a superset of the training one.
  • Do not use use JVMTI agents that call the AddToBootstrapClassLoaderSearchand AddToSystemClassLoaderSearch APIs.

To check if your JVM is correctly configured to use the AOT cache, you can add the option -XX:AOTMode=on to the command line:

java -XX:AOTCache=app.aot -XX:AOTMode=on \
     -cp app.jar com.example.App ...

The JVM will report an error if the AOT cache does not exist or if your setup disregards any of the above requirements. Furthermore, the features introduced in JDK 24 and 25 did not support the Z Garbage Collector (ZGC). Yet, this limitation no longer applies as of JDK 26, with the introduction of JEP 516: Ahead-of-Time Object Caching with Any GC.

To ensure the AOT cache works effectively in production, the training run and all following runs must be essentially identical. Training runs are a way of observing what an application is doing across different runs and are primarily two types:

  • integration tests, which run at build time
  • production workloads, which require training in production.

Avoid loading unused classes during the training step and skip rich test frameworks to keep the AOT cache minimal. Mock external dependencies in training to load needed classes, but be aware that this may introduce extra cache entries.

AOT cache effectiveness depends on how closely the training run matches production behavior. If you rebuild the application or upgrade its JDK, you must regenerate the AOT cache. Otherwise, you risk crashes or undefined behavior (methods missing from cache).

In case you need to debug the performance of your application, run it with -Xlog:aot,class+path=info to monitor what it loads from cache.

Tips for Efficient Training Runs

There is a trade-off between performance and how easy it is to run the training. Using a production run for training is not always practical, especially for server applications, which can create log files, open network connections, access databases, etc. For such cases, it is better to make a synthetic training run that closely resembles actual production runs.

Aligning the training run to load the same classes as production helps to achieve an optimized startup time. To determine which classes are loaded by your training run, you can append the -verbose:class flag upon launching it. Or observe the loaded classes by enabling the jdk.ClassLoad JFR event and profiling your application with it:

# configure the event
jfr configure jdk.ClassLoad#enabled=true

# profile as soon as your application launches
java -XX:StartFlightRecording:settings=custom.jfc,duration=60s,filename=/tmp/AOT.jfr

# profile on a running application identified through llvmid
jcmd llvmid JFR.start settings=custom.jfc duration=60s filename=/tmp/AOT.jfr

On the recording file, you may check the loaded classes, but also which methods your application frequently uses by running the following jfr commands:

# print jdk.ClassLoad events from a recording file
jfr print --events "jdk.ClassLoad" /tmp/AOT.jfr

# view frequently executed methods
jfr view hot-methods /tmp/AOT.jfr

If you determine that there are methods frequently used but not detected by your training run, exercise them. You can work out the standard modes of your application using a temporary file directory, a local network configuration, and a mocked database, if needed.
Avoid loading unused classes during training and skip rich test frameworks to keep the AOT cache minimal. Instead, use smoke tests to cover typical startup paths; avoid extensive suites and stress/regression tests.

Takeways

To conclude, crafting an AOT cache for better performance requires you to look over:

  • Cache validity or staleness; if you rebuild the application or upgrade the JDK, you must regenerate the AOT cache.
  • Portability, as the AOT cache is JVM and platform-specific.
  • Startup path coverage; the training run must cover typical application startup paths. If your training run is shallow, you will not warm up enough, and the benefits of the cache will be limited.
  • Operational setup as both the application JAR and the AOT cache must run with least privilege and according to immutable infrastructure practices.

Application performance is an ongoing task because software evolves: new features are added, libraries change, workloads grow, and infrastructure shifts (e.g., to the cloud, container orchestration, etc.). Depending on those evolutions, your application performance goals evolve as well. Invest in training your application today and keep up with JDK releases to unlock available optimizations, as performance improves with each of them!

Author: Ana-Maria Mihalceanu

Ana is a Java Champion Alumni, Developer Advocate, guest author of the book “DevOps tools for Java Developers”, and a constant adopter of challenging technical scenarios involving Java-based frameworks and multiple cloud providers. She actively supports technical communities’ growth through knowledge sharing and enjoys curating content for conferences as a program committee member. To learn more about/from her, follow her on Twitter @ammbra1508 or on Mastodon @ammbra1508.mastodon.social.

Next Post

Previous Post

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2025 JVM Advent | Powered by steinhauer.software Logosteinhauer.software

Theme by Anders Norén