
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 read, and thousands of .class files must be parsed, loaded, and linked. Loading means: the JVM turns the class from its .class file into its internal data structure. Linking means: it verifies the class, prepares its static fields, and resolves its references to other classes – only then can the class be initialized. 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 on a small test program.
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 27. 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);
}
Compile the code:
javac AotTest.java
Create a JAR file:
jar cvf AotTest.jar AotTest.class
Then start the training run with the following command:
java -XX:AOTMode=record -XX:AOTConfiguration=AotTest.conf \
-cp AotTest.jar AotTest
This creates the configuration file AotTest.conf. On Java 24, this was still a text file listing all loaded classes, which you could read in a text editor; since Java 25, it is a binary format that only the JVM itself reads back in.
Next, create the AOT 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.jar
And finally, you start the application, specifying the cache file to use:
java -XX:AOTCache=AotTest.aot -cp AotTest.jar AotTest
You have now seen all three steps. The following diagram contrasts the two ways to the cache once more – the two separate commands and the combined command from Java 25:
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 AotTest
On an Apple M5 Pro with Java 27, this call takes 45.0 milliseconds (median).
And now a call with the cache:
time java -XX:AOTCache=AotTest.aot -cp AotTest.jar AotTest
With the cache, it takes 34.7 milliseconds – the start is 23% faster. On Java 24, 25, and 26, the saving is 22, 18, and 23%.
So the example does not reach the 42% from JEP 483. The JDK developers measured a different program, HelloStream, and compared two Java versions: 31 milliseconds on Java 23 without a cache, 18 milliseconds on Java 24 with one. On the M5 Pro, the same program takes 29.7 milliseconds on Java 23 and 21.8 milliseconds on Java 24 with a cache – 26% less. How much the cache saves therefore also depends on the system the application starts on.
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. On Java 24, you could still see this in the configuration file: its header declared it to be the class list for 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 shortened the application’s startup 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.
The following diagram shows, version by version, what the cache has taken on since CDS – up to the native code proposed for Java 28:
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%. My example program starts 23% faster with the cache on Java 27.
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 this, JEP 544: Ahead-of-Time Code Compilation is targeted to Java 28 and has already been integrated into the JDK 28 codebase. According to the JEP, on a two-core machine the AOT cache reduces the startup time of the tested applications by 50 to 70% without AOT code, and by 65 to 80% with AOT code.
What should I cover next? The best way for me to find out is through your feedback: a review on my ProvenExpert profile shows me which topics matter to you – and motivates me to write more articles.




