1. Introduction
In this article, we’ll look at the Java Ahead of Time (AOT) Compiler, which is described in JEP-295 and was added as an experimental feature in Java 9.
First, we’ll see what AOT is, and second, we’ll look at a simple example. Third, we’ll see some restrictions of AOT, and lastly, we’ll discuss some possible use cases.
2. What is Ahead of Time Compilation?
AOT compilation is one way of improving the performance of Java programs and in particular the startup time of the JVM. The JVM executes Java bytecode and compiles frequently executed code to native code. This is called Just-in-Time (JIT) Compilation. The JVM decides which code to JIT compile based on profiling information collected during execution.
While this technique enables the JVM to produce highly optimized code and improves peak performance, the startup time is likely not optimal, as the executed code is not yet JIT compiled. AOT aims to improve this so-called warming-up period. The compiler used for AOT is Graal.
In this article, we won’t look at JIT and Graal in detail. Please refer to our other articles for an overview of performance improvements in Java 9 and 10, as well as a deep dive into the Graal JIT Compiler.
3. Example
For this example, we’ll use a very simple class, compile it, and see how to use the resulting library.
3.1. AOT Compilation
Let’s take a quick look at our sample class:
public class JaotCompilation { public static void main(String[] argv) { System.out.println(message()); } public static String message() { return "The JAOT compiler says 'Hello'"; } }
Before we can use the AOT compiler, we need to compile the class with the Java compiler:
javac JaotCompilation.java
We then pass the resulting JaotCompilation.class to the AOT compiler, which is located in the same directory as the standard Java compiler:
jaotc --output jaotCompilation.so JaotCompilation.class
This produces the library jaotCompilation.so in the current directory.
3.2. Running the Program
We can then execute the program:
java -XX:AOTLibrary=./jaotCompilation.so JaotCompilation
The argument -XX:AOTLibrary accepts a relative or full path to the library. Alternatively, we can copy the library to the lib folder in the Java home directory and only pass the name of the library.
3.3. Verifying That the Library Is Called and Used
We can see that the library was indeed loaded by adding -XX:+PrintAOT as a JVM argument:
java -XX:+PrintAOT -XX:AOTLibrary=./jaotCompilation.so JaotCompilation
The output will look like:
77 1 loaded ./jaotCompilation.so aot library
However, this only tells us that the library was loaded, but not that it was actually used. By passing the argument -verbose, we can see that the methods in the library are indeed called:
java -XX:AOTLibrary=./jaotCompilation.so -verbose -XX:+PrintAOT JaotCompilation
The output will contain the lines:
11 1 loaded ./jaotCompilation.so aot library 116 1 aot[ 1] jaotc.JaotCompilation.<init>()V 116 2 aot[ 1] jaotc.JaotCompilation.message()Ljava/lang/String; 116 3 aot[ 1] jaotc.JaotCompilation.main([Ljava/lang/String;)V The JAOT compiler says 'Hello'
The AOT compiled library contains a class fingerprint, which must match the fingerprint of the .class file.
Let’s change the code in the class JaotCompilation.java to return a different message:
public static String message() { return "The JAOT compiler says 'Good morning'"; }
If we execute the program without AOT compiling the modified class:
java -XX:AOTLibrary=./jaotCompilation.so -verbose -XX:+PrintAOT JaotCompilation
Then the output will contain only:
11 1 loaded ./jaotCompilation.so aot library The JAOT compiler says 'Good morning'
We can see that the methods in the library won’t be called, as the bytecode of the class has changed. The idea behind this is that the program will always produce the same result, no matter if an AOT compiled library is loaded or not.
4. More AOT and JVM Arguments
4.1. AOT Compilation of Java Modules
It’s also possible to AOT compile a module:
jaotc --output javaBase.so --module java.base
The resulting library javaBase.so is about 320 MB in size and takes some time to load. The size can be reduced by selecting the packages and classes to be AOT compiled.
We’ll look at how to do that below, however, we’ll not dive deeply into all the details.
4.2. Selective Compilation with Compile Commands
To prevent the AOT compiled library of a Java module from becoming too large, we can add compile commands to limit the scope of what gets AOT compiled. These commands need to be in a text file – in our example, we’ll use the file complileCommands.txt:
compileOnly java.lang.*
Then, we add it to the compile command:
jaotc --output javaBaseLang.so --module java.base --compile-commands compileCommands.txt
The resulting library will only contain the AOT compiled classes in the package java.lang.
To gain real performance improvement, we need to find out which classes are invoked during the warm-up of the JVM.
This can be achieved by adding several JVM arguments:
java -XX:+UnlockDiagnosticVMOptions -XX:+LogTouchedMethods -XX:+PrintTouchedMethodsAtExit JaotCompilation
In this article, we won’t dive deeper into this technique.
4.3. AOT Compilation of a Single Class
We can compile a single class with the argument –class-name:
jaotc --output javaBaseString.so --class-name java.lang.String
The resulting library will only contain the class String.
4.4. Compile for Tiered
By default, the AOT compiled code will always be used, and no JIT compilation will happen for the classes included in the library. If we want to include the profiling information in the library, we can add the argument compile-for-tiered:
jaotc --output jaotCompilation.so --compile-for-tiered JaotCompilation.class
The pre-compiled code in the library will be used until the bytecode becomes eligible for JIT compilation.
5. Possible Use Cases for AOT Compilation
One use case for AOT is short running programs, which finish execution before any JIT compilation occurs.
Another use case is embedded environments, where JIT isn’t possible.
At this point, we also need to note that the AOT compiled library can only be loaded from a Java class with identical bytecode, thus it cannot be loaded via JNI.
6. Conclusion
In this article, we saw how to AOT compile Java classes and modules. As this is still an experimental feature, the AOT compiler isn’t part of all distributions. Real examples are still rare to find, and it will be up to the Java community to find the best use cases for applying AOT.
All the code snippets in this article can be found in our GitHub repository.