Перейти к основному содержимому

Опережающая компиляция (AoT)

· 8 мин. чтения

1. Введение

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

Во-первых, мы увидим, что такое AOT, а во-вторых, рассмотрим простой пример. В-третьих, мы увидим некоторые ограничения AOT, и, наконец, мы обсудим некоторые возможные варианты использования.

2. Что такое предварительная компиляция?

Компиляция AOT — это один из способов повысить производительность программ Java и, в частности, время запуска JVM . JVM выполняет байт-код Java и компилирует часто выполняемый код в собственный код. Это называется компиляцией Just-in-Time (JIT). JVM решает, какой код компилировать JIT, на основе информации о профилировании, собранной во время выполнения.

Хотя этот метод позволяет JVM создавать высокооптимизированный код и повышает пиковую производительность, время запуска, вероятно, не является оптимальным, поскольку исполняемый код еще не скомпилирован JIT. AOT стремится улучшить так называемый период прогрева . Для AOT используется компилятор Graal.

В этой статье мы не будем подробно рассматривать JIT и Graal. Пожалуйста, обратитесь к другим нашим статьям, чтобы получить обзор улучшений производительности в Java 9 и 10 , а также подробное описание Graal JIT Compiler .

3. Пример

В этом примере мы будем использовать очень простой класс, скомпилируем его и посмотрим, как использовать получившуюся библиотеку.

3.1. Сборник АОТ

Давайте быстро взглянем на наш образец класса:

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.<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'

Скомпилированная библиотека 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 МБ, и ее загрузка занимает некоторое время. Размер можно уменьшить, выбрав пакеты и классы для компиляции AOT.

Ниже мы рассмотрим, как это сделать, однако не будем углубляться во все детали.

4.2. Выборочная компиляция с командами компиляции

Чтобы не допустить, чтобы скомпилированная AOT библиотека модуля Java стала слишком большой, мы можем добавить команды компиляции, чтобы ограничить область того, что компилируется AOT. Эти команды должны быть в текстовом файле — в нашем примере мы будем использовать файл complileCommands.txt :

compileOnly java.lang.*

Затем мы добавляем его в команду компиляции:

jaotc --output javaBaseLang.so --module java.base --compile-commands compileCommands.txt

Полученная библиотека будет содержать только скомпилированные AOT классы в пакете java.lang .

Чтобы получить реальное улучшение производительности, нам нужно выяснить, какие классы вызываются во время прогрева JVM .

Этого можно добиться, добавив несколько аргументов JVM:

java -XX:+UnlockDiagnosticVMOptions -XX:+LogTouchedMethods -XX:+PrintTouchedMethodsAtExit JaotCompilation

В этой статье мы не будем углубляться в эту технику.

4.3. Компиляция AOT одного класса

Мы можем скомпилировать один класс с аргументом –class-name :

jaotc --output javaBaseString.so --class-name java.lang.String

Полученная библиотека будет содержать только класс String .

4.4. Компиляция для многоуровневого

По умолчанию всегда будет использоваться скомпилированный код AOT, и JIT-компиляции для классов, включенных в библиотеку, не будет. Если мы хотим включить информацию о профилировании в библиотеку, мы можем добавить аргумент compile-for-tiered :

jaotc --output jaotCompilation.so --compile-for-tiered JaotCompilation.class

Предварительно скомпилированный код в библиотеке будет использоваться до тех пор, пока байт-код не станет пригодным для JIT-компиляции.

5. Возможные варианты использования компиляции AOT

Одним из вариантов использования AOT являются короткие программы, которые завершают выполнение до того, как произойдет какая-либо JIT-компиляция.

Другой вариант использования — встроенные среды, где JIT невозможен.

На этом этапе мы также должны отметить, что скомпилированная библиотека AOT может быть загружена только из класса Java с идентичным байт-кодом, поэтому ее нельзя загрузить через JNI.

6. AOT и Amazon Lambda

Возможным вариантом использования кода, скомпилированного с помощью AOT, являются недолговечные лямбда-функции, для которых важно короткое время запуска. В этом разделе мы рассмотрим, как мы можем запускать скомпилированный AOT код Java на AWS Lambda.

Для компиляции AOT с AWS Lambda требуется, чтобы библиотека была собрана в операционной системе, совместимой с операционной системой, используемой в AWS. На момент написания статьи это Amazon Linux 2 .

Кроме того, версия Java должна совпадать. AWS предоставляет Amazon Corretto Java 11 JVM . Чтобы иметь среду для компиляции нашей библиотеки, мы установим Amazon Linux 2 и Amazon Corretto в Docker.

Мы не будем обсуждать все детали использования Docker и AWS Lambda, а только наметим самые важные шаги. Для получения дополнительной информации о том, как использовать Docker, обратитесь к его официальной документации здесь .

Дополнительные сведения о создании функции Lambda с помощью Java см. в нашей статье AWS Lambda With Java .

6.1. Конфигурация нашей среды разработки

Во-первых, нам нужно загрузить образ Docker для Amazon Linux 2 и установить 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. Скомпилируйте класс и библиотеку

Внутри нашего контейнера Docker мы выполняем следующие команды:

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

Имя папки является только примером и, конечно, может быть любым другим именем.

package jaotc;

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

Следующим шагом будет компиляция класса и библиотеки:

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

Здесь важно использовать тот же сборщик мусора, что и на AWS. Если наша библиотека не может быть загружена на AWS Lambda, мы можем проверить, какой сборщик мусора фактически используется, с помощью следующей команды:

java -XX:+PrintCommandLineFlags -version

Теперь мы можем создать zip-файл, содержащий нашу библиотеку и файл класса:

zip -r jaot.zip jaotCompilation.so jaotc/

6.3. Настройка AWS Lambda

Последний шаг — войти в консоль AWS Lamda, загрузить zip-файл и настроить Lambda со следующими параметрами:

  • Время выполнения: Java 11
  • Обработчик: jaotc.JaotCompilation::message

Кроме того, нам нужно создать переменную среды с именем JAVA_TOOL_OPTIONS и установить для нее значение:

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

Эта переменная позволяет нам передавать параметры в JVM.

Последний шаг — настроить ввод для нашей лямбды. По умолчанию используется ввод JSON, который нельзя передать нашей функции, поэтому нам нужно установить его в строку, содержащую целое число, например «1».

Наконец, мы можем выполнить нашу функцию Lambda и должны увидеть в журнале, что наша скомпилированная библиотека AOT была загружена:

57    1     loaded    ./jaotCompilation.so  aot library

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

В этой статье мы увидели, как AOT компилировать классы и модули Java. Поскольку это все еще экспериментальная функция, компилятор AOT не входит во все дистрибутивы. Реальные примеры все еще редко можно найти, и сообщество Java должно найти лучшие варианты использования для применения AOT.

Все фрагменты кода в этой статье можно найти в нашем репозитории GitHub .