Understanding Ahead-of-Time (AOT) Caching in Java
Java applications have traditionally relied heavily on runtime class loading, linking, interpretation, profiling, and Just-in-Time (JIT) compilation. These mechanisms provide excellent long-running performance, but they also introduce startup and warm-up costs. The Ahead-of-Time (AOT) Cache introduced in modern Java releases addresses part of this problem by allowing the Java Virtual Machine (JVM) to perform selected work during a training run and reuse the resulting artifacts during subsequent production runs. This article explains what an AOT cache is, how to build one, how to shape the cache around an application’s workload, and the practices that should be followed when using AOT caching in production.
1. Why Java Startup Performance Matters
Java applications normally perform a considerable amount of work when a JVM starts. Classes must be discovered, loaded, verified, and linked. Objects required during startup may also need to be initialized. For a large enterprise application, this can become significant. A Spring Boot application, for example, may load thousands of classes before it is ready to process its first request. This becomes particularly important in environments where applications are frequently started or restarted:
- Containerized applications
- Kubernetes workloads
- Serverless applications
- Autoscaled microservices
- Short-lived command-line applications
- Applications where startup latency affects user experience
Traditional Class Data Sharing (CDS) already helps reduce some startup work. The newer AOT cache extends this idea by recording application-specific information during a training run. The AOT cache was introduced in JDK 24 as part of Project Leyden. The cache can contain artifacts such as loaded and linked classes and Java heap objects. Future JDK releases are also expected to use the AOT cache for additional artifacts such as execution profiles and compiled methods.
2. Understanding Ahead-of-Time (AOT) Caching
An Ahead-of-Time Cache is a file containing artifacts that the JVM can reuse when starting an application. Instead of performing all startup-related work from scratch every time, the JVM can use information generated during a previous training run. Conceptually, the process looks like this: Application → JVM starts → Discover classes → Load classes → Link classes → Initialize runtime → Application ready. With an AOT cache, some of this work is performed ahead of time: Training run → Observe startup behavior → Create AOT configuration → Build AOT cache → Production startup → Reuse cached artifacts → Application ready faster.
2.1 What Is Stored in an AOT Cache?
The exact contents evolve with the JDK, but the AOT cache introduced in JDK 24 contains application-related classes and Java objects produced by AOT optimizations. The important idea is that the cache stores the result of work that would otherwise have to be repeated during application startup.
2.2 AOT Caching vs. JIT Compilation
AOT caching should not be confused with JIT compilation.
| Feature | AOT Cache | JIT Compilation |
|---|---|---|
| When work happens | Before production startup | During application execution |
| Main goal | Reduce startup and warm-up cost | Optimize frequently executed code |
| Input | Training/application behavior | Runtime execution profiles |
| Persistence | Stored in an AOT cache | Normally generated during the JVM run |
| Scope | JVM/application startup optimization | Runtime execution optimization |
AOT and JIT therefore complement each other. AOT caching attempts to move some work earlier, while the JIT continues to optimize code based on actual runtime behavior.
2.3 How AOT Caching Works
An AOT cache deployment can be understood as three phases:
- Training: Run the application with a representative workload and collect information about what happens during startup and execution.
- Assembly: Use the collected configuration to create the AOT cache.
- Production:Start the application using the generated AOT cache.
JDK 25 introduced JEP 514, which simplifies the common case by allowing training and cache creation to be performed through a single command.
3. Getting Started with AOT Caching
The most important part of AOT caching is not simply creating a cache. The quality of the cache depends heavily on the workload used during the training phase. The training workload should represent the way the application normally starts and behaves in production.
3.1 Example Java Application
import java.util.ArrayList;
import java.util.List;
public class AotCacheDemo {
public static void main(String[] args) {
System.out.println("Application starting...");
List < String > users = loadUsers();
processUsers(users);
System.out.println("Application ready.");
}
private static List < String > loadUsers() {
List < String > users = new ArrayList < >();
users.add("Alice");
users.add("Bob");
users.add("Charlie");
return users;
}
private static void processUsers(List < String > users) {
for (String user: users) {
System.out.println("Processing user: " + user);
}
}
}
This Java program defines a class named AotCacheDemo that demonstrates a simple application workflow using a list of users. The main() method first prints "Application starting...", then calls the loadUsers() method to create an ArrayList containing the names Alice, Bob, and Charlie. The returned list is passed to the processUsers() method, which uses an enhanced for loop to iterate through each user and print "Processing user: " followed by the user’s name. After all users have been processed, control returns to the main() method, which prints "Application ready.". The program uses the List interface and ArrayList implementation from the Java Collections Framework, while List<String> ensures that the list stores only string values. The command javac AotCacheDemo.java compiles the Java source file named AotCacheDemo.java using the Java compiler and generates the corresponding bytecode file, typically AotCacheDemo.class. After successful compilation, the command java AotCacheDemo starts the Java Virtual Machine (JVM), loads the compiled AotCacheDemo class, and executes its main() method, allowing the application to run normally and display its output in the terminal.
Application starting... Processing user: Alice Processing user: Bob Processing user: Charlie Application ready.
4. Creating an AOT Cache in JDK 24
With JDK 24, the explicit AOT cache workflow consists of two main steps: a training (recording) phase and a cache creation phase. The training run allows the JVM to observe which classes are loaded and collect configuration information that can later be used to prepare the AOT cache.
First, run the application in recording mode:
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconfig -cp . AotCacheDemo
The -XX:AOTMode=record option enables the recording phase, while -XX:AOTConfiguration=app.aotconfig specifies the file where the collected AOT configuration is stored. The -cp . option places the current directory on the classpath so the JVM can locate AotCacheDemo. For a real application, the training run should exercise representative startup behavior so the configuration captures the classes commonly needed during startup.
Next, use the recorded configuration to create the AOT cache:
java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconfig -XX:AOTCache=app.aot -cp app.jar
Here, -XX:AOTMode=create tells the JVM to build the cache from the previously recorded configuration, and -XX:AOTCacheOutput=app.aot specifies the output file. The resulting app.aot file contains cached application metadata that the JVM can reuse during subsequent launches to reduce work performed at startup.
The application can then be started using the generated cache:
java -XX:AOTCache=app.aot -cp . AotCacheDemo
The -XX:AOTCache=app.aot option instructs the JVM to load the generated cache during startup. No changes to the application’s Java source code are required because the AOT optimization is configured entirely through JVM command-line options. This makes it possible to add AOT caching to an existing application without modifying its application logic.
5. Simplified AOT Cache Creation in JDK 25
JDK 25 introduced a simpler AOT cache workflow through JEP 514: Ahead-of-Time Command-Line Ergonomics. Instead of explicitly performing separate recording and cache-creation commands, the JVM can coordinate the required steps through a single command. This reduces the amount of AOT-specific configuration that developers need to manage.
To create the AOT cache, run:
java \
-XX:AOTCacheOutput=app.aot \
-cp . \
AotCacheDemoThe -XX:AOTCacheOutput=app.aot option tells the JVM to perform a training run and produce the AOT cache named app.aot. Internally, the JVM handles the recording and cache-assembly process that previously required separate commands and an intermediate AOT configuration file. The -cp . option adds the current directory to the classpath so that AotCacheDemo can be located.
Once the cache has been generated, the application can be started with it using:
java \
-XX:AOTCache=app.aot \
-cp . \
AotCacheDemoThe -XX:AOTCache=app.aot option instructs the JVM to load the generated cache at startup, allowing previously cached application information to be reused and potentially reducing startup work. As with the JDK 24 workflow, no Java source-code changes are required. For applications whose build and deployment environments use JDK 25 or later, this streamlined workflow is generally more convenient because it removes the need to manually manage the intermediate AOT configuration and cache-creation steps.
6. Conclusion
Ahead-of-Time (AOT) Cache represents an important advancement in Java startup performance by allowing the JVM to perform selected class-loading, linking, and initialization-related work during a training phase and reuse the resulting artifacts during later application launches, rather than repeating the same startup work for every JVM instance. The overall process follows a simple sequence: Training → Observe Application → Create AOT Cache → Production → Reuse Cached Startup Artifacts. JDK 24 introduced the AOT cache along with an explicit training and cache-creation workflow, while JDK 25 simplified the common process through JEP 514 and the -XX:AOTCacheOutput option. An AOT cache should not be treated as a universally reusable file because it is associated with the application and its runtime environment, and its effectiveness depends heavily on how representative the training workload is. This makes AOT caching particularly useful for applications that experience frequent JVM startups, including microservices, Kubernetes workloads, serverless applications, and autoscaled services, where faster startup and reduced warm-up work can improve responsiveness and scaling behavior. A practical production approach is to measure startup performance, use a representative training workload, generate the cache as part of the build or release process, validate the resulting startup behavior, and regenerate it whenever significant application or runtime changes occur. AOT caching does not replace JIT compilation, application-level caching, or other Java performance optimizations; instead, it complements them by shifting selected startup work from production execution to an earlier stage of the application lifecycle.

