Compilazione in anticipo (AoT)

1. Introduzione

In questo articolo, esamineremo il compilatore Java Ahead of Time (AOT), descritto in JEP-295 ed è stato aggiunto come funzionalità sperimentale in Java 9.

In primo luogo, vedremo cos'è AOT e, in secondo luogo, esamineremo un semplice esempio. Terzo, vedremo alcune limitazioni di AOT e, infine, discuteremo alcuni possibili casi d'uso.

2. Che cosa è prima della compilazione del tempo?

La compilazione AOT è un modo per migliorare le prestazioni dei programmi Java e in particolare il tempo di avvio della JVM . La JVM esegue il bytecode Java e compila il codice eseguito di frequente in codice nativo. Questa è chiamata compilazione Just-in-Time (JIT). La JVM decide quale codice compilare JIT in base alle informazioni di profilazione raccolte durante l'esecuzione.

Sebbene questa tecnica consenta alla JVM di produrre codice altamente ottimizzato e migliori le prestazioni massime, il tempo di avvio probabilmente non è ottimale, poiché il codice eseguito non è ancora compilato JIT. AOT mira a migliorare questo cosiddetto periodo di riscaldamento . Il compilatore utilizzato per AOT è Graal.

In questo articolo, non esamineremo in dettaglio JIT e Graal. Fare riferimento ai nostri altri articoli per una panoramica dei miglioramenti delle prestazioni in Java 9 e 10, nonché per un'immersione approfondita nel compilatore JIT Graal.

3. Esempio

Per questo esempio, useremo una classe molto semplice, la compileremo e vedremo come usare la libreria risultante.

3.1. Compilazione AOT

Diamo una rapida occhiata alla nostra classe di esempio:

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

Prima di poter utilizzare il compilatore AOT, dobbiamo compilare la classe con il compilatore Java:

javac JaotCompilation.java 

Quindi passiamo il JaotCompilation.class risultante al compilatore AOT, che si trova nella stessa directory del compilatore Java standard:

jaotc --output jaotCompilation.so JaotCompilation.class 

Questo produce la libreria jaotCompilation.so nella directory corrente.

3.2. Esecuzione del programma

Possiamo quindi eseguire il programma:

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

L'argomento -XX: AOTLibrary accetta un percorso relativo o completo alla libreria. In alternativa, possiamo copiare la libreria nella cartella lib nella home directory di Java e passare solo il nome della libreria.

3.3. Verifica che la libreria sia chiamata e utilizzata

Possiamo vedere che la libreria è stata effettivamente caricata aggiungendo -XX: + PrintAOT come argomento JVM:

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

L'output sarà simile a:

77 1 loaded ./jaotCompilation.so aot library 

Tuttavia, questo ci dice solo che la libreria è stata caricata, ma non che è stata effettivamente utilizzata. Passando l'argomento -verbose , possiamo vedere che i metodi nella libreria sono effettivamente chiamati:

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

L'output conterrà le righe:

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' 

La libreria compilata AOT contiene un'impronta digitale della classe , che deve corrispondere all'impronta digitale del file .class .

Cambiamo il codice nella classe JaotCompilation.java per restituire un messaggio diverso:

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

Se eseguiamo il programma senza che AOT compili la classe modificata:

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

Quindi l'output conterrà solo:

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

Possiamo vedere che i metodi nella libreria non verranno chiamati, poiché il bytecode della classe è cambiato. L'idea alla base di questo è che il programma produrrà sempre lo stesso risultato, indipendentemente dal fatto che una libreria compilata AOT sia caricata o meno.

4. Più argomenti AOT e JVM

4.1. Compilazione AOT di moduli Java

È anche possibile compilare un modulo AOT:

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

La libreria risultante javaBase.so ha una dimensione di circa 320 MB e richiede un po 'di tempo per caricarsi. La dimensione può essere ridotta selezionando i pacchetti e le classi da compilare AOT.

Vedremo come farlo di seguito, tuttavia, non approfondiremo tutti i dettagli.

4.2. Compilazione selettiva con comandi di compilazione

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

Questa variabile ci consente di passare parametri alla JVM.

L'ultimo passaggio è configurare l'ingresso per il nostro Lambda. L'impostazione predefinita è un input JSON, che non può essere passato alla nostra funzione, quindi dobbiamo impostarlo su una stringa che contiene un numero intero, ad esempio "1".

Infine, possiamo eseguire la nostra funzione Lambda e dovremmo vedere nel log che la nostra libreria compilata AOT è stata caricata:

57 1 loaded ./jaotCompilation.so aot library

7. Conclusione

In questo articolo, abbiamo visto come compilare AOT classi e moduli Java. Poiché questa è ancora una funzionalità sperimentale, il compilatore AOT non fa parte di tutte le distribuzioni. Gli esempi reali sono ancora rari da trovare e spetterà alla comunità Java trovare i migliori casi d'uso per l'applicazione di AOT.

Tutti i frammenti di codice in questo articolo sono disponibili nel nostro repository GitHub.