«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.
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.<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. Compile for Tiered
По умолчанию всегда будет использоваться код, скомпилированный 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. Заключение
В этой статье мы увидели, как компилировать классы Java AOT и модули. Поскольку это все еще экспериментальная функция, компилятор AOT не входит во все дистрибутивы. Реальные примеры все еще редко можно найти, и сообщество Java должно найти лучшие варианты использования для применения AOT.
«Все фрагменты кода в этой статье можно найти в нашем репозитории GitHub.