Ahead of Time Compilation (AoT)

1. Einleitung

In diesem Artikel sehen wir uns den Java Ahead of Time (AOT) -Compiler an, der in JEP-295 beschrieben ist und als experimentelle Funktion in Java 9 hinzugefügt wurde.

Erstens werden wir sehen, was AOT ist, und zweitens werden wir uns ein einfaches Beispiel ansehen. Drittens werden wir einige Einschränkungen von AOT sehen und schließlich werden wir einige mögliche Anwendungsfälle diskutieren.

2. Was ist der Zeitzusammenstellung voraus?

Die AOT-Kompilierung ist eine Möglichkeit, die Leistung von Java-Programmen und insbesondere die Startzeit der JVM zu verbessern . Die JVM führt Java-Bytecode aus und kompiliert häufig ausgeführten Code zu nativem Code. Dies wird als Just-in-Time-Kompilierung (JIT) bezeichnet. Die JVM entscheidet anhand der während der Ausführung gesammelten Profilinformationen, welchen Code JIT kompilieren soll.

Während diese Technik es der JVM ermöglicht, hochoptimierten Code zu erzeugen und die Spitzenleistung zu verbessern, ist die Startzeit wahrscheinlich nicht optimal, da der ausgeführte Code noch nicht JIT-kompiliert ist. AOT zielt darauf ab, diese sogenannte Aufwärmphase zu verbessern . Der für AOT verwendete Compiler ist Graal.

In diesem Artikel werden wir JIT und Graal nicht im Detail betrachten. In unseren anderen Artikeln finden Sie eine Übersicht über Leistungsverbesserungen in Java 9 und 10 sowie einen umfassenden Einblick in den Graal JIT Compiler.

3. Beispiel

In diesem Beispiel verwenden wir eine sehr einfache Klasse, kompilieren sie und sehen, wie die resultierende Bibliothek verwendet wird.

3.1. AOT-Zusammenstellung

Werfen wir einen kurzen Blick auf unsere Beispielklasse:

public class JaotCompilation { public static void main(String[] argv) { System.out.println(message()); } public static String message() { return "The JAOT compiler says 'Hello'"; } } 

Bevor wir den AOT-Compiler verwenden können, müssen wir die Klasse mit dem Java-Compiler kompilieren:

javac JaotCompilation.java 

Anschließend übergeben wir die resultierende JaotCompilation.class an den AOT-Compiler, der sich im selben Verzeichnis wie der Standard-Java-Compiler befindet:

jaotc --output jaotCompilation.so JaotCompilation.class 

Dadurch wird die Bibliothek jaotCompilation.so im aktuellen Verzeichnis erstellt.

3.2. Ausführen des Programms

Wir können dann das Programm ausführen:

java -XX:AOTLibrary=./jaotCompilation.so JaotCompilation 

Das Argument -XX: AOTLibrary akzeptiert einen relativen oder vollständigen Pfad zur Bibliothek. Alternativ können wir die Bibliothek in den lib- Ordner im Java- Ausgangsverzeichnis kopieren und nur den Namen der Bibliothek übergeben.

3.3. Überprüfen, ob die Bibliothek aufgerufen und verwendet wird

Wir können sehen, dass die Bibliothek tatsächlich geladen wurde, indem -XX: + PrintAOT als JVM-Argument hinzugefügt wurde :

java -XX:+PrintAOT -XX:AOTLibrary=./jaotCompilation.so JaotCompilation 

Die Ausgabe sieht folgendermaßen aus:

77 1 loaded ./jaotCompilation.so aot library 

Dies sagt uns jedoch nur, dass die Bibliothek geladen wurde, aber nicht, dass sie tatsächlich verwendet wurde. Wenn wir das Argument -verbose übergeben , können wir sehen, dass die Methoden in der Bibliothek tatsächlich aufgerufen werden:

java -XX:AOTLibrary=./jaotCompilation.so -verbose -XX:+PrintAOT JaotCompilation 

Die Ausgabe enthält die Zeilen:

11 1 loaded ./jaotCompilation.so aot library 116 1 aot[ 1] jaotc.JaotCompilation.()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' 

Die kompilierte AOT-Bibliothek enthält einen Klassenfingerabdruck , der mit dem Fingerabdruck der .class- Datei übereinstimmen muss .

Ändern wir den Code in der Klasse JaotCompilation.java , um eine andere Nachricht zurückzugeben:

public static String message() { return "The JAOT compiler says 'Good morning'"; } 

Wenn wir das Programm ausführen, ohne dass AOT die geänderte Klasse kompiliert:

java -XX:AOTLibrary=./jaotCompilation.so -verbose -XX:+PrintAOT JaotCompilation 

Dann enthält die Ausgabe nur:

 11 1 loaded ./jaotCompilation.so aot library The JAOT compiler says 'Good morning'

Wir können sehen, dass die Methoden in der Bibliothek nicht aufgerufen werden, da sich der Bytecode der Klasse geändert hat. Die Idee dahinter ist, dass das Programm immer das gleiche Ergebnis liefert, egal ob eine AOT-kompilierte Bibliothek geladen ist oder nicht.

4. Weitere AOT- und JVM-Argumente

4.1. AOT-Kompilierung von Java-Modulen

Es ist auch möglich, ein Modul von AOT zu kompilieren:

jaotc --output javaBase.so --module java.base 

Die resultierende Bibliothek javaBase.so ist ungefähr 320 MB groß und das Laden dauert einige Zeit. Die Größe kann reduziert werden, indem die Pakete und Klassen ausgewählt werden, die AOT kompiliert werden sollen.

Wir werden uns unten ansehen, wie das geht, aber wir werden nicht tief in alle Details eintauchen.

4.2. Selektive Kompilierung mit Kompilierungsbefehlen

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. AOT and Amazon Lambda

A possible use case for AOT-compiled code is short-lived lambda functions where short startup time is important. In this section, we'll look at how we can run AOT compiled Java code on AWS Lambda.

Using AOT compilation with AWS Lambda requires the library to be built on an operating system that is compatible with the operating system used on AWS. At the time of writing, this is Amazon Linux 2.

Furthermore, the Java version needs to match. AWS provides the Amazon Corretto Java 11 JVM. In order to have an environment to compile our library, we'll install Amazon Linux 2 and Amazon Corretto in Docker.

We won't discuss all the details of using Docker and AWS Lambda but only outline the most important steps. For more information on how to use Docker, please refer to its official documentation here.

For more details about creating a Lambda function with Java, you can have a look at our article AWS Lambda With Java.

6.1. Configuration of Our Development Environment

First, we need to pull the Docker image for Amazon Linux 2 and install Amazon Corretto:

# download Amazon Linux docker pull amazonlinux # inside the Docker container, install Amazon Corretto yum install java-11-amazon-corretto # some additional libraries needed for jaotc yum install binutils.x86_64 

6.2. Compile the Class and Library

Inside our Docker container, we execute the following commands:

# create folder aot mkdir aot cd aot mkdir jaotc cd jaotc

The name of the folder is only an example and can, of course, be any other name.

package jaotc; public class JaotCompilation { public static int message(int input) { return input * 2; } }

The next step is to compile the class and library:

javac JaotCompilation.java cd .. jaotc -J-XX:+UseSerialGC --output jaotCompilation.so jaotc/JaotCompilation.class

Here, it's important to use the same garbage collector as is used on AWS. If our library cannot be loaded on AWS Lambda, we might want to check which garbage collector is actually used with the following command:

java -XX:+PrintCommandLineFlags -version

Now, we can create a zip file that contains our library and class file:

zip -r jaot.zip jaotCompilation.so jaotc/

6.3. Configure AWS Lambda

The last step is to log into the AWS Lamda console, upload the zip file and configure out Lambda with the following parameters:

  • Runtime: Java 11
  • Handler: jaotc.JaotCompilation::message

Furthermore, we need to create an environment variable with the name JAVA_TOOL_OPTIONS and set its value to:

-XX:+UnlockExperimentalVMOptions -XX:+PrintAOT -XX:AOTLibrary=./jaotCompilation.so

Mit dieser Variablen können wir Parameter an die JVM übergeben.

Der letzte Schritt besteht darin, den Eingang für unser Lambda zu konfigurieren. Der Standardwert ist eine JSON-Eingabe, die nicht an unsere Funktion übergeben werden kann. Daher müssen wir sie auf einen String setzen, der eine Ganzzahl enthält, z. B. "1".

Schließlich können wir unsere Lambda-Funktion ausführen und sollten im Protokoll sehen, dass unsere AOT-kompilierte Bibliothek geladen wurde:

57 1 loaded ./jaotCompilation.so aot library

7. Fazit

In diesem Artikel haben wir gesehen, wie AOT Java-Klassen und -Module kompiliert. Da dies noch eine experimentelle Funktion ist, ist der AOT-Compiler nicht Teil aller Distributionen. Echte Beispiele sind immer noch selten zu finden, und es liegt an der Java-Community, die besten Anwendungsfälle für die Anwendung von AOT zu finden.

Alle Codefragmente in diesem Artikel finden Sie in unserem GitHub-Repository.