This post is meant to serve as an introduction to Ahead Of Time (AOT) compilation in OpenJ9. It does not delve too deeply into the technical details and subtleties involved in implementing AOT compilations; these will be covered in future blog posts. In this post, AOT can refer to either the AOT Compilation or the infrastructure surrounding it.
What is an AOT Compilation?
AOT compilation, in the context of OpenJ9, generally refers to a Java method implemented as bytecodes that was compiled and stored to the Shared Class Cache (SCC). An AOT compilation is performed by the compiler in one JVM instance and subsequently gets used by the compiler in another JVM instance1. This is in direct contrast to Just In Time (JIT) compilation, which implies that the compilation of a Java bytecode method was performed by the compiler in the currently running JVM instance and not shared with any other JVM instances.
Why have AOT Compilations?
The benefits of AOT compilations are improved application startup time, and reduced CPU usage. For more information on the benefits of AOT code, take a look at some OpenJ9 Performance Results. As you can imagine, if a bunch of Java bytecode methods have already been compiled, then the compiler does not need to waste CPU cycles compiling those methods again, and instead, the JVM can just load those methods and start running them – at least that’s the simplified idea. In this manner, it is theoretically possible to get an application up to its peak steady state performance much faster because there is no need to wait for the JVM to “warm up”. But, of course, it’s more complicated than that.
How to AOT?
At a very high level, making use of AOT involves two main steps:
- Populate the SCC with AOT compiled code
- Load the AOT compiled code and run it
Let’s take a closer look at these two steps.
Populate the SCC with AOT compiled code
To populate the SCC with AOT compiled code at least one JVM needs to actually do the work to compile all the Java bytecode methods. Thus, while AOT does save CPU usage and improves startup, that is only true for every subsequent JVM invocation, known as the “warm run”; the very first run in which the JVM does the compilation work is known as the “cold run”.
Now that we’ve sorted out that technicality, we need to deal with the more fundamental problem of getting AOT compilations functional. If a Java bytecode method was compiled by one JVM, how can a different JVM run that code as is? Well, in order to allow another JVM to run AOT code, two actions need to be performed.
- All assumptions made during the compilation of a method need to be validated to ensure they are still true.
- All appropriate code locations need to be relocated in order to be capable of running in a different address space.
Thus, a fundamental difference between an AOT and a JIT compilation is that the former requires the creation of Validation and Relocation Records.
Validation Records are used to ensure that the assumptions made about the environment in the “cold run” are consistent with the environment in the “warm run”. One example is to ensure that the class hierarchy of some class in the “warm run” is the same as what it was in the “cold run”. An even simpler example is to ensure that the architecture version of the machine where the AOT code is loaded is compatible with the code2.
Relocation Records are used to relocate (i.e. update) all locations in the code body that contain references to the old address space (i.e. the address space of the “cold run”) with the appropriate references that are valid in the current address space (i.e. the address space of the “warm run”). Thus, this ensures that any references to java classes, java methods, or static addresses are valid in the “warm run”.
With these Relocation and Validation records, the compiler can now generate Relocatable Code, which can be stored into the SCC for use by a different JVM.
Load the AOT compiled code and run it
As mentioned above, because the code stored in the SCC is not valid in the current environment, the JVM that loads this code needs to first relocate the code before it can run it. This involves first going through all the Validation Records to ensure that all the assumptions made in the “cold run” are still valid, and then going through all the Relocation Records to materialize the data that is valid in the current environment and patching the appropriate locations (specified by the Relocation Records) with the valid data.
With this done, the JVM can now run the code.
An Important Distinction
Static AOT involves taking a collection of classes and compiling every method in those classes prior to running any java code. Dynamic AOT, on the other hand, involves determining what methods to compile based on runtime characteristics.
OpenJ9 implements Dynamic AOT. Just as with JIT compilations, the compiler only AOT compiles methods that have been executed a certain number of times.
Hopefully this gives you a general understanding of the AOT process. If you’re interested in the deep technical details, stay tuned for further blog posts.
1. Actually, the JVM instance that compiles the AOT code also loads and runs that code. However, the code is in a form that allows it to also be loaded and run by other JVM instances.
2. In OpenJ9, this isn’t validated using a Validation Record, but rather through something known as the Feature Flags which is beyond the scope of this blog post but will be described in future blog posts.