Inside JDK 24: Understanding Ahead-of-Time Class Loading & Linking
Ahead-of-Time Class Loading & Linking is one of the JEPs being introduced with JDK 24. Its primary goal is to enhance the startup time of JVM applications in the most seamless manner. You can utilize this feature without any additional code changes or extra tools. You can simply activate it by passing a single option during application startup. Essentially, it builds upon the foundation laid by Class Data Sharing and serves as a continuation of that feature.
JVM startup
To gain a better understanding of how AOT (Ahead-Of-Time) Class Loading & Linking works, it's essential first to grasp how the startup process of a JVM (Java Virtual Machine) application unfolds:
When you invoke the JNI_CreateJavaVM
function (which is done automatically when using a simple command like java -jar myapp.jar
) the JVM first allocates memory for various internal structures, including the heap and method area, etc.. Following this, it initializes components such as the Garbage Collector and JIT (Just-In-Time) compiler.
Next, the JVM begins loading classes using the Bootstrap Class Loader. This loader is responsible for classes critical to the Java environment, including:
- Object,
- String,
- ClassLoader (the Bootstrap Class Loader is a specific type of class loader that's part of the core JVM, implemented in native code),
- Thread,
- and much more.
After loading, the JVM verifies and prepares these classes. Depending on the JVM’s strategy (lazy or eager), it may resolve symbolic references at this stage. Once verified and prepared, the classes are initialized and ready for use.
Ahead-of-Time Class Loading & Linking in action
In general, a significant amount of work needs to be done before we reach our main method. This is why applications running on the JVM tend to start much slower than those written in languages like C++ or Rust. Class Data Sharing (CDS) was introduced some time ago to mitigate part of this issue. CDS, which is enabled by default starting from JDK 12, addresses steps required for further class loading. It uses an archive file as a cache for parsed class bytecode. The JVM can then utilize classes from this archive in subsequent startups without needing to parse them again. This cache is shared among different applications and only includes core JDK classes. There is also a variation known as Application Class Data Sharing, which expands this archive by caching application-specific classes.
However, class loading is just one part of the JVM startup process. JEP 483 takes a step further with AOT Class Loading & Linking. Although CDS addresses some class loading issues, it handles this differently. CDS keeps classes in a parsed state within its cache, allowing them to be quickly loaded into memory one at a time, but the linking step must still be performed afterward.
In contrast, the current improvement involves gathering all classes during a training run (which will be explained later), ensuring that they are verified, prepared, and have their symbolic links resolved. Essentially, these classes are linked and ready for use. This means that the AOT cache contains significantly more data, which is loaded into memory all at once (for a detailed explanation of the processes involved, refer to the Project Leyden Update video). That is because, to initialize any class, the entire class sub-graph must be resolved, meaning that all superclasses and interfaces need to be linked together before a specific class can be used.
Training run
The assumption is that the data stored in the AOT cache will not change between the training and production runs. What does "training run" mean, and what could it be? It depends on our specific needs. A good candidate for a training run might be a production run itself, but integration tests could serve as even better candidates. Thus, you can build the AOT cache during your project build. However, you need to be cautious and avoid loading classes that will not be used in production. Classes loaded solely for testing purposes are a prime example of this. According to JEP documentation, future updates may introduce additional filtering capabilities to assist with this task.
It is essential to understand that the more closely the training run aligns with the production run, the more beneficial it will be. However, it's not an issue if some classes are not included in the AOT cache. In these cases, the JVM will load them using the standard method. This adaptability is one of the greatest strengths of this feature, in my opinion. It has the potential to offer significant benefits while maintaining the dynamism of the JVM. Additionally, it is easy to use, as no extra tools are needed for the process. To take advantage of what this JEP offers, all you need to do is include single option in both your training and production runs:
-XX:CacheDataStore=myapp.aot
I won’t present specific measurements regarding the improvements this JEP offers, but you can find more information on JEP 483's official page or the blog post where I compared different methods for speeding up JVM startup. It's important to note that the benefits you can gain from using this new feature largely depend on your application's specific characteristics.
Even though this JEP is marked as Closed/Delivered, there are still potential improvements to consider for the future:
- the ability to filter out classes that should not be included in the AOT cache,
- support for user-defined class loaders, as currently only classes loaded by built-in class loaders can be part of the AOT cache.
Conclusion
JEP 483 represents another step toward addressing slow JVM startup times. I hadn’t mentioned it earlier, but this initiative is part of the larger Project Leyden, and this is the first JEP that has been completed. Two more JEPs are expected to follow:
Additionally, as indicated on the JEP web page, there may still be possible improvements in the area of class loading and linking in the future.
Reviewed by Szymon Winiarz