Пред компилация на времето (AoT)

1. Въведение

В тази статия ще разгледаме Java Ahead of Time (AOT) Compiler, който е описан в JEP-295 и е добавен като експериментална функция в Java 9.

Първо, ще видим какво е AOT, и второ, ще разгледаме един прост пример. Трето, ще видим някои ограничения на AOT и накрая ще обсъдим някои възможни случаи на употреба.

2. Какво предстои за компилация на времето?

AOT компилацията е един от начините за подобряване на производителността на Java програми и по-специално времето за стартиране на JVM . JVM изпълнява Java байт код и компилира често изпълнявания код в роден код. Това се нарича Just-in-Time (JIT) Compilation. JVM решава кой код да компилира въз основа на информация за профилиране, събрана по време на изпълнение.

Докато тази техника позволява на JVM да произвежда силно оптимизиран код и подобрява пикова производителност, времето за стартиране вероятно не е оптимално, тъй като изпълненият код все още не е компилиран JIT. AOT има за цел да подобри този така наречен период на загряване . Компилаторът, използван за AOT, е Graal.

В тази статия няма да разглеждаме подробно JIT и Graal. Моля, обърнете се към другите ни статии за преглед на подобренията на производителността в Java 9 и 10, както и задълбочено потапяне в компилатора Graal JIT.

3. Пример

За този пример ще използваме много прост клас, ще го компилираме и ще видим как да използваме получената библиотека.

3.1. AOT Компилация

Нека да разгледаме набързо нашия примерен клас:

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

Преди да можем да използваме AOT компилатора, трябва да компилираме класа с Java компилатора:

javac JaotCompilation.java 

След това предаваме получения резултат JaotCompilation.class на AOT компилатора, който се намира в същата директория като стандартния Java компилатор:

jaotc --output jaotCompilation.so JaotCompilation.class 

Това създава библиотеката jaotCompilation.so в текущата директория.

3.2. Стартиране на програмата

След това можем да изпълним програмата:

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

Аргументът -XX: AOTLibrary приема относителна или пълна пътека към библиотеката. Като алтернатива можем да копираме библиотеката в папката lib в домашната директория на Java и само да предадем името на библиотеката.

3.3. Проверка, че библиотеката е извикана и използвана

Можем да видим, че библиотеката наистина е била заредена чрез добавяне на -XX: + PrintAOT като JVM аргумент:

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

Резултатът ще изглежда така:

77 1 loaded ./jaotCompilation.so aot library 

Това обаче ни казва само, че библиотеката е заредена, но не и че всъщност е била използвана. Предавайки аргумента -verbose , можем да видим, че методите в библиотеката наистина се извикват:

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

Резултатът ще съдържа редовете:

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' 

Компилираната библиотека AOT съдържа пръстов отпечатък на класа , който трябва да съвпада с пръстовия отпечатък на файла .class .

Нека променим кода в класа JaotCompilation.java, за да върнем различно съобщение:

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

Ако изпълним програмата без AOT да компилира модифицирания клас:

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

Тогава изходът ще съдържа само:

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

Виждаме, че методите в библиотеката няма да бъдат извикани, тъй като байт кодът на класа се е променил. Идеята зад това е, че програмата винаги ще дава един и същ резултат, без значение дали AOT компилирана библиотека е заредена или не.

4. Още аргументи за AOT и JVM

4.1. AOT Компилация на Java модули

Също така е възможно AOT да компилира модул:

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

Получената библиотека javaBase.so е с размер около 320 MB и отнема известно време, за да се зареди. Размерът може да бъде намален, като изберете пакетите и класовете, които да бъдат AOT компилирани.

Ще разгледаме как да го направим по-долу, но няма да се задълбочим във всички подробности.

4.2. Селективна компилация с компилиращи команди

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

Тази променлива ни позволява да предаваме параметри на JVM.

Последната стъпка е да конфигурирате входа за нашата ламбда. По подразбиране е JSON вход, който не може да бъде предаден на нашата функция, поради което трябва да го зададем на String, който съдържа цяло число, например „1“.

И накрая, можем да изпълним нашата функция Lambda и трябва да видим в дневника, че нашата AOT компилирана библиотека е заредена:

57 1 loaded ./jaotCompilation.so aot library

7. Заключение

В тази статия видяхме как AOT да компилира Java класове и модули. Тъй като това все още е експериментална функция, компилаторът AOT не е част от всички дистрибуции. Реални примери все още се срещат рядко и от общността на Java ще зависи най-добрите случаи на приложение за прилагане на AOT.

Всички кодови фрагменти в тази статия могат да бъдат намерени в нашето хранилище на GitHub.