

In this article, you will learn:
- Why do large Java applications take several seconds to start?
- What is Ahead-of-Time Class Loading & Linking, and how can it improve startup time?
- Step by step: How can Ahead-of-Time Class Loading & Linking accelerate the start of an application?
- How does AOT Class Loading & Linking differ from (App)CDS?
Why Do Java Applications Start So Slowly?
Java applications are extremely flexible at run time. Classes can be loaded and unloaded dynamically. Dynamic compilation, optimization, and deoptimization make Java programs as fast as C code – or faster. And reflection is what makes frameworks like Jakarta EE, Spring Boot, Quarkus, Helidon, and Micronaut possible in the first place.
But these advantages come at a price:
When starting an application, hundreds of .jar files must be unpacked, and thousands of .class files must be loaded into memory, analyzed, and linked. The static initialization code of classes must be executed, and frameworks like Jakarta EE and Spring must scan the code for annotations, instantiate beans, and execute configuration code.
Large backend applications can thus take several seconds or even minutes to start.
How Can Application Startup Be Accelerated?
Many of the initialization tasks described in the previous section are the same for each application start.
As part of Project Leyden, work is underway to perform as many of these repetitive tasks as possible before starting an application.
With JDK Enhancement Proposal 483, the first fruits of this work were released in Java 24: after being read, parsed, loaded, and linked, classes can now be stored in a binary file – the AOT cache – so that they are available much faster, in a loaded and linked state, on future starts of the same application. The JDK developers have measured startup time reductions of up to 42%.
In Java 25, JDK Enhancement Proposal 514, Ahead-of-Time Command-Line Ergonomics, simplified the generation of the AOT cache: instead of two steps, only one needs to be executed.
With JDK Enhancement Proposal 515, Ahead-of-Time Method Profiling, Java 25 also extended the AOT cache with statistics about method calls, so that the JVM can compile the most frequently called methods (“hotspots”) shortly after startup, instead of having to collect profiling data at run time first. As a result, the application reaches peak performance faster – a reduction in warmup time of 19% was measured.
And in Java 26, JDK Enhancement Proposal 516, Ahead-of-Time Object Caching with Any GC, added another building block: until then, the AOT cache's pre-initialized objects were mapped directly into memory in a GC-specific format – which didn't work with every garbage collector. As of Java 26, these objects can instead be loaded sequentially in a GC-neutral format. As a result, the AOT cache can now be used with any garbage collector – including the low-latency collector ZGC, which had been left out until now.
How Does Ahead-of-Time Class Loading & Linking Work?
To accelerate program startup through AOT Class Loading & Linking, we need to perform three steps (or two – more on that below):
- In the first step, the application is started in a so-called training run. During this, the JVM analyzes all loaded and linked classes and generates a configuration file with the relevant information about these classes – and from Java 25 onwards also about method call statistics.
- In the second step, the binary cache file is created using this configuration file.
- For each subsequent application start, you specify this cache file, and the application loads the classes in loaded and linked form directly from this cache – and from Java 25 onwards, it starts directly with the optimization of the most frequently called methods (“hotspots”).
This procedure sounds more complicated than it is. In the following, I'll walk you through it step by step with an example application, all the way to a faster startup.
Step-by-Step Instructions to Follow Along
In this section, I'll show you how Ahead-of-Time Class Loading & Linking works, using a small application.
We’ll use a simple demo program that just displays the current time.
Download a current JDK – ideally Java 25 (LTS) or Java 26. Ahead-of-Time Class Loading & Linking has been available since Java 24; the compact source files with an instance main method used in this example have been final since Java 25, and the -XX:AOTCacheOutput option, which lets us combine the first two steps into one, is also available as of Java 25.
Save the following source code in a file named AotTest.java:
void main() {
var now = LocalDateTime.now();
var nowString = now.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
System.out.println("Hello, it's " + nowString);
}Code language: Java (java)Compile the code:
javac AotTest.javaCode language: plaintext (plaintext)Create a JAR file:
jar cvf AotTest.jar AotTest.classCode language: plaintext (plaintext)Then start the training run with the following command:
java -XX:AOTMode=record -XX:AOTConfiguration=AotTest.conf \
-cp AotTest.jar AotTestCode language: plaintext (plaintext)This creates the configuration file AotTest.conf. You can open it in a text editor – it contains a long list of classes and data about those classes.
Next, create the class cache in the file AotTest.aot with the following command (the application is not run again here):
java -XX:AOTMode=create -XX:AOTConfiguration=AotTest.conf -XX:AOTCache=AotTest.aot \
-cp AotTest.jarCode language: plaintext (plaintext)And finally, you start the application, specifying the cache file to use:
java -XX:AOTCache=AotTest.aot -cp AotTest.jar AotTestCode language: plaintext (plaintext)Let's now compare the application's startup time with and without the cache.
First, a call without the cache:
time java -cp AotTest.jar AotTestCode language: plaintext (plaintext)Across five runs, I got a mean runtime of 0.137 seconds.
And now a call with the cache:
time java -XX:AOTCache=AotTest.aot -cp AotTest.jar AotTestCode language: plaintext (plaintext)Here, five runs yielded a mean runtime of 0.086 seconds. That's an impressive performance gain of 37%, coming close to the 42% measured by the developers.
And What About AppCDS?
You might be wondering now: What's the difference between Ahead-of-Time Class Loading & Linking and (Application) Class Data Sharing?
Class Data Sharing (CDS) has existed since Java 5 and allows storing the JDK classes in a platform-specific binary format, from which the classes can then be loaded much faster than from .class files.
In Java 10, Application Class Data Sharing (AppCDS) was added, which allows not only JDK classes but also application classes to be stored in this binary format.
In fact, Ahead-of-Time Class Loading & Linking builds upon (App)CDS. If you looked at the file AotTest.conf earlier, you might have noticed that the header says it’s a “CDS archive dump.”
While Class Data Sharing merely reads and parses the classes and then stores them in a binary format, with AOT Class Loading & Linking, the classes are additionally – as the name suggests – loaded into Class objects and linked.
The Leyden developers tested both mechanisms with the Spring PetClinic. AppCDS sped up the application's load time by 33%, and Ahead-of-Time Class Loading & Linking by 42%. So if you're already using AppCDS, the startup improvement from Ahead-of-Time Class Loading & Linking will be somewhat less significant.
Conclusion
Java is a very flexible and powerful language, but this flexibility can lead to startup times ranging from several seconds to minutes for larger applications. During startup, Java classes are read, parsed, loaded, and linked, among other things.
With Ahead-of-Time Class Loading & Linking, we can perform these steps once before the application starts, thereby – according to the developers of this feature – accelerating the actual start of the application by up to 42%.
With Ahead-of-Time Method Profiling, statistics about method calls are also stored in the AOT cache, so that frequently called methods (“hotspots”) can be optimized right after the application starts. And since Java 26, the AOT cache's object caching works with any garbage collector – including the low-latency collector ZGC.
This isn't the end of the road, though: the next step taking shape in Project Leyden is Ahead-of-Time Code Compilation – precompiling frequently used methods into native code, which is then likewise stored in the AOT cache. For now, however, this exists only as a JEP draft: Ahead-of-Time Code Compilation, which has not yet been assigned to any Java version.
If you try out this feature, please write about your experiences in the comments. I’m curious about your measurements and your opinion!