openjdk.ruOpenJDK на русском

JEP 295: Ahead-of-Time Compilation

Компиляция Ahead-of-Time (заблаговременная компиляция и подготовка)

ОтветственныйVladimir Kozlov
ТипFeature
ОбластьImplementation
СтатусClosed / Delivered
Выпуск9
Компонентhotspot / compiler
Обсуждениеhotspot dash compiler dash dev at openjdk dot java dot net
ТрудоёмкостьM
ДлительностьM
РецензентыJohn Rose, Mikael Vidstedt
ОдобренJohn Rose
Создан2016/09/15 01:20
Обновлён2018/10/05 22:52
Задача8166089

Аннотация

Компиляция классов Java в машинный код до запуска виртуальной машины.

Цели

  • Сократить время запуска как небольших, так и крупных приложений Java при не более чем ограниченном влиянии на пиковую производительность.

  • Как можно меньше менять порядок работы конечного пользователя.

Что не является целью

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

Мотивация

JIT-компиляторы работают быстро, но программы Java могут становиться настолько большими, что полный прогрев JIT занимает много времени. Редко используемые методы Java могут вообще никогда не скомпилироваться, что может приводить к потерям производительности из-за повторных вызовов в режиме интерпретации.

Описание

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

Чтобы использовать модуль java.base, скомпилированный AOT, пользователю нужно будет скомпилировать модуль и скопировать получившуюся AOT-библиотеку в каталог установки JDK или указать её в командной строке java. В остальном использование AOT-скомпилированного кода полностью прозрачно для конечных пользователей.

AOT-компиляция выполняется новым инструментом jaotc:

jaotc --output libHelloWorld.so HelloWorld.class
jaotc --output libjava.base.so --module java.base

В качестве бэкенда генерации кода он использует Graal.

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

java -XX:AOTLibrary=./libHelloWorld.so,./libjava.base.so HelloWorld

Новые флаги AOT для java и флаги jaotc перечислены в следующих подразделах, где также есть инструкции по сборке и установке AOT-библиотеки для модуля java.base.

В качестве контейнерного формата для AOT-скомпилированного кода используются общие библиотеки. Версия JDK 9 поддерживает только Linux/x64, где формат общих библиотек — ELF. AOT-скомпилированный код в AOT-библиотеках JVM рассматривает как расширение существующего CodeCache. При загрузке класса java JVM проверяет, есть ли в загруженных AOT-библиотеках соответствующие AOT-скомпилированные методы, и добавляет ссылки на них из дескрипторов методов java. К AOT-скомпилированному коду применяются те же правила вызова, деоптимизации и выгрузки, что и к обычному JIT-скомпилированному коду.

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

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

jaotc не разрешает ссылки на классы, которые не являются системными классами или частью компилируемых классов. Их нужно добавить в class path. Иначе во время AOT-компиляции может быть выброшено исключение ClassNotFoundException.

jaotc --output=libfoo.so --jar foo.jar -J-cp -J./
jaotc --output=libactivation.so --module java.activation -J--add-module=java.se.ee

Использование AOT

Для AOT-компиляции используйте инструмент jaotc. Этот инструмент входит в установку java — так же, как javac.

jaotc --output libHelloWorld.so HelloWorld.class

Затем укажите сгенерированную AOT-библиотеку при выполнении приложения:

java -XX:AOTLibrary=./libHelloWorld.so HelloWorld

В этом выпуске при AOT-компиляции и при выполнении следует использовать одну и ту же конфигурацию среды выполнения java. Например:

jaotc -J-XX:+UseParallelGC -J-XX:-UseCompressedOops --output libHelloWorld.so HelloWorld.class 
java -XX:+UseParallelGC -XX:-UseCompressedOops -XX:AOTLibrary=./libHelloWorld.so HelloWorld

Это включает требование использовать один и тот же вариант сборки JDK: product или debug.

Конфигурация среды выполнения записывается в AOT-библиотеку и проверяется при загрузке библиотеки во время выполнения. Если проверка не прошла, эта AOT-библиотека не будет использоваться, и JVM продолжит работу или завершится, если указан флаг -XX:+UseAOTStrictLoading.

Во время запуска JVM код инициализации AOT ищет общие библиотеки с известными именами в известном расположении или библиотеки, указанные параметром -XX:AOTLibrary. Если общие библиотеки найдены, они подхватываются и используются. Если общие библиотеки найти не удаётся, AOT будет отключён для этого запуска экземпляра JVM.

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

  • Код, скомпилированный в режиме non-tiered AOT, ведёт себя подобно статически скомпилированному коду C++: профилирующая информация не собирается, и JIT-перекомпиляции не происходят.
  • Код, скомпилированный в режиме tiered AOT, собирает профилирующую информацию. Выполняемое профилирование такое же, как простое профилирование в методах, скомпилированных C1 на уровне Tier 2. Если AOT-методы достигают порогов вызовов AOT, эти методы сначала перекомпилируются C1 на уровне Tier 3, чтобы собрать полную профилирующую информацию. Она нужна для JIT-перекомпиляций C2, чтобы получить оптимальный код и достичь пиковой производительности приложения.

Дополнительный шаг перекомпиляции кода на уровне Tier 3 необходим, так как накладные расходы полного профилирования слишком велики, чтобы применять его ко всем методам, особенно к методам такого модуля, как java.base. Для пользовательских приложений может иметь смысл разрешить AOT-компиляцию с профилированием, эквивалентным Tier 3, но в JDK 9 это поддерживаться не будет.

Логичный режим компиляции для java.base — tiered AOT, поскольку для достижения пиковой производительности желательна JIT-перекомпиляция методов java.base. Non-tiered AOT-компиляция имеет смысл лишь в определённых сценариях. К ним относятся приложения, которым нужно предсказуемое поведение, случаи, когда объём занимаемой памяти важнее пиковой производительности, и системы, где динамическая генерация кода не допускается. В этих случаях AOT-компиляцию нужно выполнять для всего приложения, и поэтому в JDK 9 она экспериментальная.

Набор AOT-библиотек может быть сгенерирован для разных сред выполнения. JVM знает следующие имена well-known для AOT-библиотек java.base, сгенерированных для конкретной конфигурации среды выполнения. Она будет искать их в каталоге $JAVA_HOME/lib и загрузит ту, которая соответствует текущей конфигурации среды выполнения:

-XX:-UseCompressedOops -XX:+UseG1GC :       libjava.base.so
-XX:+UseCompressedOops -XX:+UseG1GC :       libjava.base-coop.so
-XX:-UseCompressedOops -XX:+UseParallelGC : libjava.base-nong1.so
-XX:+UseCompressedOops -XX:+UseParallelGC : libjava.base-coop-nong1.so

JVM также знает имена AOT-библиотек для следующих модулей java, но их компиляция, установка и использование экспериментальные:

java.base
jdk.compiler 
jdk.scripting.nashorn 
jdk.internal.vm.ci 
jdk.internal.vm.compiler

Шаги по генерации и использованию AOT-библиотеки для модуля java.base

Скомпилируйте модуль java.base с помощью jaotc. Для этого нужна большая куча java, чтобы хранить данные всех компилируемых методов (около 50000 методов):

jaotc -J-XX:+UseCompressedOops -J-XX:+UseG1GC -J-Xmx4g --compile-for-tiered --info --compile-commands java.base-list.txt --output libjava.base-coop.so --module java.base

Некоторые методы в java.base приводят к сбою компиляции и исключаются с помощью параметра --compile-comands:

cat java.base-list.txt

# jaotc: java.lang.StackOverflowError
exclude sun.util.resources.LocaleNames.getContents()[[Ljava/lang/Object;
exclude sun.util.resources.TimeZoneNames.getContents()[[Ljava/lang/Object;
exclude sun.util.resources.cldr.LocaleNames.getContents()[[Ljava/lang/Object;
exclude sun.util.resources..*.LocaleNames_.*.getContents\(\)\[\[Ljava/lang/Object;
exclude sun.util.resources..*.LocaleNames_.*_.*.getContents\(\)\[\[Ljava/lang/Object;
exclude sun.util.resources..*.TimeZoneNames_.*.getContents\(\)\[\[Ljava/lang/Object;
exclude sun.util.resources..*.TimeZoneNames_.*_.*.getContents\(\)\[\[Ljava/lang/Object;
# java.lang.Error: Trampoline must not be defined by the bootstrap classloader
exclude sun.reflect.misc.Trampoline.<clinit>()V
exclude sun.reflect.misc.Trampoline.invoke(Ljava/lang/reflect/Method;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
# JVM asserts
exclude com.sun.crypto.provider.AESWrapCipher.engineUnwrap([BLjava/lang/String;I)Ljava/security/Key;
exclude sun.security.ssl.*
exclude sun.net.RegisteredDomain.<clinit>()V
# Huge methods
exclude jdk.internal.module.SystemModules.descriptors()[Ljava/lang/module/ModuleDescriptor;

После генерации AOT-библиотеки укажите её при выполнении приложения с помощью параметра -XX:AOTLibrary (в JDK 9 java по умолчанию использует G1 и compressed oops — указывать эти флаги не нужно):

java -XX:AOTLibrary=./libjava.base-coop.so,./libHelloWorld.so HelloWorld

Или скопируйте сгенерированную AOT-библиотеку в каталог установки JDK (возможно, потребуется изменить права доступа к каталогу):

cp libjava.base-coop.so $JAVA_HOME/lib/

В этом случае она будет загружаться автоматически, без указания в командной строке:

java -XX:AOTLibrary=./libHelloWorld.so HelloWorld

Рассмотрите возможность применить strip к неиспользуемым символам AOT-библиотеки, чтобы уменьшить размер библиотеки.

Новые флаги AOT для среды выполнения

-XX:+/-UseAOT

Использовать AOT-скомпилированные файлы. По умолчанию включено.

-XX:AOTLibrary=<file>

Задаёт список файлов AOT-библиотек. Записи библиотек разделяются двоеточиями (:) или запятой (,).

-XX:+/-PrintAOT

Выводит используемые AOT-классы и методы.

Доступен дополнительный диагностический флаг (требует указать флаг -XX:+UnlockDiagnosticVMOptions):

-XX:+/-UseAOTStrictLoading

Завершить работу JVM, если у какой-либо из AOT-библиотек конфигурация среды выполнения не соответствует текущим настройкам среды выполнения.

Среда выполнения JVM поддерживает следующие теги AOT для Unified Logging, интегрированные с JEP 158: Unified JVM Logging.

aotclassfingerprint

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

aotclassload

записывать в журнал, когда соответствующие данные класса найдены в AOT-библиотеке.

aotclassresolve

записывать в журнал, был ли успешным запрос от AOT-скомпилированного кода на разрешение класса.

jaotc: компилятор Java Ahead-of-Time

jaotc — статический компилятор java, который генерирует машинный код для компилируемых методов java. В качестве бэкенда генерации кода он использует Graal, а для генерации AOT-библиотеки .so — libelf.

Инструмент входит в установку java и может использоваться так же, как javac.

jaotc <options> <name or list>

Здесь «name» — имя класса или jar-файла. «list» — разделённый символом : список имён классов, модулей, jar-файлов или каталогов, содержащих файлы классов.

Доступны следующие параметры jaotc:

--output <file>

Имя выходного файла. Имя по умолчанию — «unnamed.so».

--class-name <class names>

Список компилируемых классов Java

--jar <jar files>

Список компилируемых jar-файлов

--module <modules>

Список компилируемых модулей Java

--directory <dirs>

Список каталогов, в которых искать файлы для компиляции

--search-path <dirs>

Список каталогов, в которых искать указанные файлы

--compile-commands <file>

Имя файла с командами компиляции:

exclude sun.util.resources..*.TimeZoneNames_.*.getContents\(\)\[\[Ljava/lang/Object; 
exclude sun.security.ssl.* 
compileOnly java.lang.String.*

В настоящее время AOT распознаёт две команды компиляции:

exclude       - exclude compilation of specified methods 
compileOnly   - compile only specified methods

Классы и методы задаются регулярными выражениями.

--compile-for-tiered

Генерировать профилирующий код для многоуровневой компиляции. По умолчанию профилирующий код не генерируется (в будущем это может измениться).

--compile-with-assertions

Генерировать код с assertions java. По умолчанию код assertions не генерируется.

--compile-threads <number>

Число используемых потоков компиляции. Значение по умолчанию: min(16, available_cpus).

--ignore-errors

Игнорирует все исключения, выброшенные при загрузке классов. По умолчанию компиляция завершается, если при загрузке класса выбрасывается исключение.

--exit-on-error

Завершить работу при ошибках компиляции. По умолчанию неудавшаяся компиляция пропускается, и компиляция остальных методов продолжается.

--info

Выводит информацию о фазах компиляции

--verbose

Выводит больше подробностей о фазах компиляции, включает флаг --info

--debug

Выводит ещё больше подробностей, включает флаги --info и --verbose

--help

Выводит сообщение об использовании jaotc и флаги

--version

Выводит информацию о версии

-J<flag>

Передаёт flag напрямую исполняющей системе JVM

Текущие ограничения AOT

  • Первый выпуск AOT в JDK 9 предназначен только для экспериментального использования и работает только в системах Linux x64 с 64-битной Java и Parallel GC или G1.
  • AOT-компиляцию нужно выполнять в той же системе, где Java-приложение будет использовать AOT-код, или в системе с такой же конфигурацией.
  • При AOT-компиляции и при выполнении должна использоваться одна и та же конфигурация среды выполнения Java. Например, если приложение будет использовать Parallel GC вместе с AOT-кодом, инструмент jaotc тоже нужно запускать с Parallel GC, указав флаг -J. Несовпадение конфигураций среды выполнения может привести к аварийному завершению приложения во время выполнения.
  • Java-код, в котором используются динамически генерируемые классы и байт-код (лямбда-выражения, invoke dynamic), может не компилироваться. Любой код, не скомпилированный AOT, во время выполнения обрабатывается обычным образом: сначала выполняется интерпретатором, а затем компилируется JIT-компилятором.
  • AOT не поддерживает пользовательские загрузчики классов, поскольку при AOT-компиляции о них нет информации. Методы, скомпилированные AOT, не будут использоваться для классов, загруженных невстроенными загрузчиками.

Эти ограничения могут быть устранены в будущих выпусках.

Альтернативы

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

Тестирование

Для тестирования функциональности AOT будут разработаны новые тесты jtreg для AOT.

Все существующие тесты можно запускать на JDK с включённым AOT. Это уже делается в виде отдельных конфигураций ночного тестирования.

Другая конфигурация запускает все тесты на JDK с включённым AOT и модулем java.base, скомпилированным AOT.

Риски и допущения

Возможно, что из-за использования заранее скомпилированного кода будет выполняться не самый оптимальный код, что приведёт к потере производительности. Тестирование производительности показывает, что одни приложения выигрывают от кода, скомпилированного AOT, а у других явно наблюдается регрессия. Поскольку AOT включается только по желанию, возможных регрессий производительности пользовательских приложений можно избежать. Если пользователь обнаружит, что приложение запускается медленнее, не достигает ожидаемой пиковой производительности или аварийно завершается, он может просто отключить AOT флагом -XX:-UseAOT или удалить все AOT-библиотеки.

Рекомендуется выполнять AOT-компиляцию в доверенных средах, где библиотеки и инструменты JDK защищены от несанкционированного изменения.

Зависимости

Этот проект зависит от JEP 243: Java-Level JVM Compiler Interface, поскольку AOT-компилятор использует Graal в качестве бэкенда генерации кода, а Graal, в свою очередь, зависит от JVMCI.

Проект включит ядро Graal в JDK и будет поставлять его в сборках для Linux/x64.