Pangram verdict · v3.3
We believe this text is mainly human-written, with some AI content.
AI likelihood · overall
HumanArticle text · 1,324 words · 14 segments analyzed
Introduction The AOT (ahead-of-time) cache is the JDK's newest approach to improving startup and warmup times of Java applications. The idea is simple: You perform a training run of your application, during which the JVM records what your application does, e.g. which classes it loads and links and which methods it calls. When your application is later started, the JVM can skip a lot of the initialization work as it is already done and immediately has all the classes available in a loaded and linked state. In theory, this is a free startup performance boost without having to change anything in your application. In practice, using it for a real-world application that is distributed to users, like we do at XPipe, comes with a lot of pitfalls as we had to learn. In the best case, the AOT cache is just not applied due to incompatibilities and you gain nothing. In the worst case, if you are not careful, your JVM crashes unexpectedly. So essentially a free startup boost, but only under certain conditions, and with free crashes included. Hopefully this post will prevent you from running into the same issues as we did while rolling out this feature. At the end, you can find the build scripts we use for our applications based on all the things we learned over time. Note that this post does not contain any performance measurements or comparisons, as that is a whole another story. This post is already too long for what it covers. The concepts If you are familiar with the JDK AOT concepts, you can skip this section. If not, you can get a short overview over all the relevant parts that go into the topic of AOT optimizations. Timeline The AOT cache itself has been extended with every JDK release since its introduction, with each step specified in a JEP (JDK Enhancement Proposal): JDK 24: JEP 483: Ahead-of-Time Class Loading & Linking introduced the AOT cache itself JDK 25: JEP 514: Ahead-of-Time Command-Line Ergonomics simplified the cache creation to a single step with -XX:AOTCacheOutput, and JEP 515: Ahead-of-Time Method Profiling added method profiles to the cache so that the JIT compiler can warm up faster JDK 26: JEP 516: Ahead-of-Time Object Caching with Any GC made cached objects usable with any garbage collector JDK 28: JEP 544: Ahead-of-Time Code Compilation will also add compiled code to the cache Class-data sharing (CDS) The AOT cache is not built from scratch. It is a natural evolution of class-data sharing (CDS), a HotSpot feature that has existed since JDK 5.
CDS stores parsed class metadata in an archive file that the JVM can directly map into memory on startup.
It was extended over time with application class-data sharing in JDK 10, a default CDS archive for JDK classes that is shipped with every JDK since JDK 12, and dynamic CDS archives in JDK 13. The AOT cache uses the same infrastructure, which is why you will still see CDS terms like "shared archive" or the cds log tag in many places when working with the AOT cache. AOT class linking AOT cache and AOT class linking are often used interchangeably, but they are not the same thing. The AOT cache is the file, which can contain different kinds of data: The loaded class data (the part inherited from CDS), method profiles, cached Java objects, some native code, and pre-linked classes.
AOT class linking is just one of these optimizations.
It is turned on automatically when creating an AOT cache (-XX:+AOTClassLinking) and stores classes in an already linked state, so that the JVM can skip that work on startup. You want to enable class linking, otherwise you lose out on startup performance gains. GraalVM Native Image The AOT cache should also not be confused with GraalVM Native Image.
Native Image compiles your whole application ahead of time into a standalone native executable that runs without a JVM.
To do that, it has to make a closed-world assumption: All code that can be executed at runtime has to be known at build time, so dynamic features like reflection, dynamic class loading, or resources have to be explicitly configured. In return, you get a very fast startup and low memory usage. The AOT cache takes a different approach.
Your application still runs on a normal JVM with the JIT compiler and has the full dynamic capabilities of Java, so you don't have to change or configure anything in your application.
The cache only speeds up the work the JVM has to do anyway.
If it can't be used, the JVM just falls back to doing all of that at runtime as usual.
The startup improvements are therefore smaller than with Native Image, but you also don't have to deal with any of its restrictions.
If you ever had to troubleshoot GraalVM native image issues, you are probably familiar with those. CPU incompatibilities In theory, the whole AOT cache idea sounds like something that should only affect the class data and not perform any native compilation. Because if it did, you would run into all sorts of issues if the individual CPU features of the training system and actual system would differ. Even if the CPU architecture is the same. Anyone who used GraalVM native image and ran into crashes on systems with missing CPU features is probably familiar with this and the GraalVM build argument -march=compatibility. In practice, it turns out the AOT cache with class linking does generate small parts of native code. You will run into the consequences of this if your training system supports something like AVX, and the running system does not (or not to the same level, like no support for AVX-512, which corresponds to the JDK's -XX:UseAVX=3 level). Because then, your JVM will crash with: A fatal error has been detected by the Java Runtime Environment: SIGILL (0x4) at pc=0x00007fcc85ae2613, pid=4054, tid=4147 JRE version: OpenJDK Runtime Environment Zulu25.32+21-CA (25.0.2+10) (build 25.0.2+10-LTS) Java VM: OpenJDK 64-Bit Server VM Zulu25.32+21-CA (25.0.2+10-LTS, mixed mode, sharing, tiered, compressed oops, compact obj headers, g1 gc, linux-amd64) Problematic frame: v ~AdapterBlob 0x00007fcc85ae2613 or # # A fatal error has been detected by the Java Runtime Environment: # # EXCEPTION_ILLEGAL_INSTRUCTION (0xc000001d) at pc=0x000001d00d9f6c2b, pid=39700, tid=37844 # # JRE version: OpenJDK Runtime Environment Zulu25.30+17-CA (25.0.1+8) (build 25.0.1+8-LTS) # Java VM: OpenJDK 64-Bit Server VM Zulu25.30+17-CA (25.0.1+8-LTS, mixed mode, sharing, tiered, compressed oops, compact obj headers, g1 gc, windows-amd64) # Problematic frame: # v ~AdapterBlob 0x000001d00d9f6c2b This means that you for example can't run an application with an AOT cache which was trained on an 13th gen Intel Core CPU on a target system that has a 12th gen Intel Core CPU as those have different levels of CPU feature support for AVX. If you only have newer gen CPUs and ship your app to a customer who still runs older gen hardware, you will get an unhappy customer who thinks that you don't even do basic testing for their software. Adapter caches As it turns out, it generates adapter caches, which were added in JDK 25 with JDK-8350209: Preserve adapters in AOT cache. Adapters are small pieces of machine code that switch between interpreted and compiled Java calls (see the HotSpot source for a more detailed explanation). They're generated for the CPU features of the machine that builds the cache without taking into account that the application might be run on another system later on. As soon as a system that lacks one of the CPU features used in the cached adapters calls one of them, the JVM crashes.
This is not only a problem for desktop applications that are distributed to users.
Even on backend servers, the AOT cache is often created in a CI pipeline or a container image build, and then deployed to machines whose CPUs differ from the build machine, e.g. when a cluster contains different instance types or the cloud provider moves your workload to other hardware.