GraalVM Native Image компилирует приложение Java в двоичный исполняемый файл. Скомпилированное таким образом приложение запускается мгновенно и обычно использует меньше памяти во время выполнения, что делает его мощным вариантом развертывания для облачных сервисов, где характеристики производительности важны для эффективного масштабирования. В этом сообщении в блоге рассказывается о некоторых полезных приемах, позволяющих сделать процесс настройки и переход от оперативного (JIT) к опережающему (AOT) режиму выполнения как можно более плавным.

Подготовьте среду
Требуется конфигурация
Ошибки времени сборки
Ошибки времени выполнения
Инструменты для собственного образа

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

Помимо некоторых советов и приемов, мы также рассмотрим несколько часто повторяющихся ошибок сборки, их причины и способы их устранения. Наконец, вы найдете краткий обзор инструментов Native Image, которые могут быть полезны для предотвращения возможных проблем или помочь в исследовании производительности.

Подготовьте свое окружение

Native Image поддерживается всеми дистрибутивами GraalVM: Community или Enterprise Edition; на основе JDK 8 или JDK 11; для платформ Linux, macOS, Windows. Однако сборки на JDK 11, как правило, более эффективны, поэтому по возможности отдайте предпочтение им.

Вам понадобятся файлы заголовков для библиотек C, например zlib и gcc, потому что построитель собственных образов зависит от локальной инструментальной цепочки. Проверьте, выполняете ли вы предварительные условия для использования Native Image для вашей платформы.

Чтобы проверить среду здания, информацию о собственной инструментальной цепочке и настройки, применяемые при создании изображения, используйте параметр --native-image-info, например:

native-image --native-image-info -jar App.jar

Чтобы определить, какой GraalVM использовался для создания собственного образа, вы можете выполнить следующую команду: strings imagename | grep com.oracle.svm.core.VM.

Требуется конфигурация

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

Этот подход работает блестяще, если только некоторые из языковых функций не являются полностью динамическими. Собственный конструктор изображений не может статически определить все пути выполнения, если ваши приложения используют, например, Java Reflection API.

Точно так же есть и другие динамические функции, которые генерируют или используют код во время выполнения, которые нельзя статически понять во время сборки: прокси, собственный интерфейс Java (JNI), некоторые виды использования дескрипторов методов, даже доступ к ресурсам пути к классам (Class.getResource). Поэтому, если в вашем коде используются эти функции, вы должны «проинформировать» конструктор изображений в машинном коде, какие классы и методы следует включить в окончательный двоичный файл.

Если какая-либо из этих функций используется без предоставления необходимой конфигурации во время сборки образа, построитель может не прервать работу по умолчанию, а сгенерировать резервный образ, который не будет иметь характеристик производительности собственного образа. Чтобы предотвратить это, создайте собственный образ с флагом --no-fallback.

Собственный образ поддерживает несколько параметров для настройки процесса сборки собственного образа. Рекомендуемый способ - встроить файл native-image.properties в файл JAR проекта. Это обычный файл свойств, поддерживающий свойства Args, JavaArgs и ImageName. Все аргументы оцениваются слева направо, и построитель автоматически выбирает все параметры конфигурации, предоставленные в любом месте ниже местоположения META-INF / native-image. Чтобы определить, какие данные конфигурации применяются для построения образа, включите подробный вывод (--verbose) или передайте параметр --native-image-info. См. Специальное руководство по внедрению файла свойств и его форматированию.

Как настроить динамические функции для нативного изображения?

Некоторые виды использования динамических функций, например, тривиальные вызовы рефлексии, обрабатываются автоматически, другие цели рефлексивного доступа должны быть предоставлены в формате JSON с файлом конфигурации. Вы должны передать этот файл в качестве аргумента native-image в файле конфигурации native-image.properties. Вот пример:

Как наиболее удобно создавать файлы конфигурации?

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

Агент отслеживает все случаи использования динамических функций во время выполнения приложения и записывает их в файлы конфигурации. Рекомендуется использовать агент. Чтобы включить агент, введите -agentlib:native-image-agent=config-output-dir=<path> перед параметром -jar, именем класса или любыми параметрами приложения в командной строке. Создайте каталог META-INF/native-image/, если он еще не существует, и запустите:

$JAVA_HOME/bin/java -agentlib:native-image-agent=config-output-dir=META-INF/native-image -jar App.jar

Агент взаимодействует с JVM для перехвата всех классов, методов, полей, ресурсов или запросов доступа к прокси. Как только процесс JVM завершается, агент генерирует подобные jni-config.json, отражать-config.json, proxy-config.json и resource-config.json и другие конфигурации файлы в указанный выходной каталог. Совсем недавно в Native Image была добавлена ​​поддержка сериализации, и агент трассировки также может предоставить список классов, используемых в действии десериализации, и вывести их в файл serialization-config.json.

Как уже упоминалось выше, native-image по умолчанию берет все файлы конфигурации из каталога META-INF / native-image на пути к классу.

Возможно, потребуется запустить целевое приложение более одного раза с разными входными данными, чтобы создать конфигурацию для разных путей выполнения. Это связано с тем, что агент отслеживает выполнение и не анализирует ваше приложение. Затем вы можете запустить агент с параметром config-merge-dir, чтобы объединить отслеживаемую конфигурацию с существующим набором файлов конфигурации:

$JAVA_HOME/bin/java -agentlib:native-image-agent=config-merge-dir=/path/to/config-directory/ -jar App.jar

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

Ошибки времени сборки

Классы, инициализированные во время сборки, а не во время выполнения

Типичная ошибка времени сборки - «Классы, которые должны быть инициализированы во время выполнения, были инициализированы во время сборки образа». Например:

Error: Classes that should be initialized at run time got initialized during image building: org.example.library.Klass the class was requested to be initialized at build time (from the command line). org.example.library.Klass has been initialized without the native-image initialization instrumentation and the stack trace can't be tracked.
...
Error: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
Error: Image build request failed with exit status 1

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

Перед использованием классы в вашем приложении необходимо инициализировать. Этот процесс, например, инициализирует статические поля класса. Жизненный цикл образа в машинном коде разделен на две части: время сборки - когда вы создаете образ в машинном коде, компилируете код и сохраняете данные в куче образа, и время выполнения - когда вы запускаете полученный исполняемый файл. Хотя между ними существует очевидная временная разница, и они, вероятно, происходят в разных средах, некоторый код вашего приложения может выполняться в обоих, поэтому вы можете думать о них как о среде выполнения вашего приложения. По умолчанию классы приложений инициализируются во время выполнения, но у вас есть возможность настроить этот параметр по умолчанию с помощью параметров --initialize-at-build-time или --initialize-at-run-time.

Конфигурация для времени инициализации классов может поступать из разных мест: встроена в файл JAR библиотеки, ваша структура может установить инициализацию некоторых классов для времени сборки, скрипт сборки, который вы используете, может настроить его для некоторых классов с помощью параметров командной строки .

И последнее, но не менее важное: инициализация времени сборки распространяется через статические поля. Это может сбивать с толку, если вы не задумывались об этом раньше. Когда вы инициализируете класс, вы инициализируете статические поля, например, назначая там объект класса. Этот класс необходимо инициализировать, прежде чем он может быть создан, тогда он также будет инициализирован во время сборки.

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

Вернемся к сообщению об ошибке выше:

Error: Classes that should be initialized at run time got initialized during image building: org.example.library.Klass the class was requested to be initialized at build time (from the command line).

Чтобы решить эту проблему, мы должны устранить несоответствие между конфигурацией при инициализации класса Klass. Решение состоит в том, чтобы определить цепочку классов, которые инициализируются Klass во время построения образа, и заставить их инициализироваться во время выполнения с помощью параметра --initialize-at-run-time.

  1. Сначала вам нужно отследить, какой класс виновен, непреднамеренно инициализирует другой класс во время сборки, добавив параметр --trace-class-initialization в команду сборки собственного образа:
native-image -jar my-app.jar 
-H:IncludeResources=resources.json
--trace-class-initialization 
--verbose

2. При запуске вы должны получить аналогичную ошибку:

Error: Classes that should be initialized at run time got initialized during image building:
 org.example.library.Klass was unintentionally initialized at build time. some.other.klass.AnotherKlass caused initialization of this class with the following trace:
        at org.example.library.Klass.<clinit>(Klass.java)
        at org.example.library.KlassFactory.<init>(KlassFactory.java:90)
        at org.example.library.SomeOtherKlass.<clinit>(SomeOtherKlass.java:86)

3. Виной всему первый класс в stacktrace. Снова соберите образ в машинном коде, инициализировав его во время выполнения:

native-image -jar my-app.jar
--initialize-at-run-time=org.example.library.SomeOtherKlass
-H:IncludeResources=resources.json
--trace-class-initialization
--verbose

Возможно, вам придется повторить описанные выше шаги, если появятся другие ошибки. Если вам нужно добавить дополнительные классы для инициализации, --initialize-at-run-time (а также параметр времени сборки) принимает список классов и имен пакетов, разделенных запятыми, или префиксы пакетов.

Общее предпочтение должно заключаться в том, чтобы инициализировать как можно больше классов во время выполнения. Инициализация классов во время сборки может иметь непредвиденные побочные эффекты, и на самом деле разработчик обязан проверить, безопасен ли класс для инициализации.

Ошибка отсутствия типа

Следующая ошибка, о которой часто сообщают, - это отсутствующий тип во время сборки:

com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: NNN

Эта ошибка вызвана отсутствием классов во время сборки образа. Поскольку среда выполнения собственных образов не включает средства для загрузки новых классов, весь код должен быть доступен и скомпилирован во время сборки. Таким образом, любой класс, на который имеется ссылка, но отсутствует, представляет собой потенциальную проблему во время выполнения. Лучший совет - предоставить все зависимости для процесса сборки. Если вы абсолютно уверены, что класс является на 100% необязательным и не будет использоваться во время выполнения, вы можете изменить поведение по умолчанию, заключающееся в сбое процесса сборки, найдя отсутствующий класс с параметром --allow-incomplete-classpath на native-image.

Недостаточно памяти

Другой проблемой при создании образов в машинном коде могут быть ошибки нехватки памяти или взаимоблокировки:

Image generator watchdog detected no activity. This can be a sign of a deadlock during image building and watchdog is aborting image generation.

Построение образа в машинном коде - это процесс с интенсивными вычислениями и потребляет некоторое количество ОЗУ: создается представление всей программы, чтобы выяснить, какие классы и методы будут использоваться во время выполнения. Построитель образов в машинном коде - это программа Java, работающая на виртуальной машине Java HotSpot и использующая управление памятью базового JDK. Чтобы предотвратить ошибки нехватки памяти, явно установите максимальный размер кучи, используемый во время построения образа, передав -J + <jvm option for memory> встроенному построителю образов. Например, native-image -J-Xmx14g.

Ошибки времени выполнения

Вы применили агента. Он, как и ожидалось, обнаружил множество классов и добавил их в файлы конфигурации. Вы успешно создали собственный образ. Затем вы выполнили свое изображение и получили ошибку ClassNotFound, хотя класс присутствовал в толстом JAR-файле. Что бы вы сделали? Эта ошибка означает, что класс с именем, напечатанным на консоли, не найден в пути к классам (недоступен в файле конфигурации). Чтобы исправить это, добавьте имя класса в reflection-config.json:

{
  {
    "name": "java.package.ClassName"
  }
}

Еще одно быстрое исправление - перестроить образ в машинном коде с параметром --allow-incomplete-classpath, чтобы переместить любые возможные ошибки связывания из времени сборки во время выполнения.

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

java.lang.ClassNotFoundException: xxx
java.io.FileNotFoundException: Could not locate xxx on classpath
java.lang.IllegalArgumentException: Class xxx is instantiated reflectively but was never registered
java.lang.IllegalArgumentException: java.lang.IllegalArgumentException: Class xxx cannot be instantiated reflectively
java.lang.InstantiationException: Type xxx can not be instantiated reflectively as it does not have a no-parameter constructor or the no-parameter constructor has not been added explicitly to the native image
java.lang.IllegalStateException: input must not be null

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

В большинстве случаев при возникновении проблемы сообщение об ошибке предлагает вам, что именно делать. Например:

Caused by: java.util.MissingResourceException: Resource bundle not found javax.servlet.http.LocalStrings. Register the resource bundle using the option -H:IncludeResourceBundles=javax.servlet.http.LocalStrings.

Таким образом, вы добавляете -H:IncludeResourceBundles=javax.servlet.http.LocalStringsto либо в командную строку, либо в файл native-image.properties и перестраиваете образ.

Если в консоли нет явного предложения, вы можете либо вручную отследить инициализацию классов (--trace-class-initialization) во время сборки образа, либо применить инструмент Агент трассировки для создания необходимой конфигурации, что является рекомендуемым подходом.

Причиной отсутствия ресурсов может быть также то, что некоторые файлы конфигурации недоступны из пути к классам (путем указания некоторого настраиваемого каталога вывода для агента -agentlib:native-image-agent=config-output-dir=/path/to/config-dir/). Информировать процесс сборки о файлах конфигурации можно несколькими способами. Например, вы можете:

  • переместите сгенерированные файлы в каталог META-INF / native-image /, который по умолчанию доступен из пути к классам, например, в каталоге src/main/resources
  • переместите их в каталог пути к классам и укажите его с аргументом: -H:ConfigurationResourceRoots=path/to/resources/
  • поместить в произвольный каталог и указать его с аргументом: -H:ConfigurationFileDirectories=/path/to/config-dir/

Инструменты для Native Image

GraalVM предлагает некоторые инструменты для Native Image, которые могут вам помочь.

Агент поиска

Агент трассировки уже упоминался в этом посте как незаменимый компонент для обеспечения необходимой конфигурации встроенному построителю образов. Он отслеживает все случаи использования динамических функций при выполнении вашего приложения на JVM и записывает их в файлы JSON для последующего использования native-image:

$JAVA_HOME/bin/java -agentlib:native-image-agent=config-output-dir=/path/to/config-dir/ -jar App.jar

Агент трассировки также может записать файл трассировки в формате JSON, trace-file.json,, который содержит каждый отдельный доступ. Дополнительные сведения см. В руководстве Вспомогательная настройка сборок собственных образов.

Панель управления GraalVM

Если вы подозреваете, что ваш собственный исполняемый файл слишком велик, или вам интересно, какие классы, пакеты или предварительно инициализированные объекты заполняют изображение и занимают большую часть кучи, вы можете исследовать это с помощью веб-инструмента визуализации - GraalVM Dashboard.
Чтобы начать использовать инструмент, соберите диагностические данные в файл дампа при построении образа, откройте панель управления и загрузите дамп. На скриншоте ниже показана разбивка по размерам пакетов:

Отладка

JDK традиционно поставляется с коннекторами для установления сеанса отладки с целевой JVM. Сгенерированный образ в машинном коде - это сильно оптимизированный код с минимальной символьной информацией, что затрудняет его отладку. Однако GraalVM позволяет создавать собственный образ с отладочной информацией Dwarf и открывать сетевые сокеты для подключения клиентов отладки. Чтобы создать образ с отладочными символами, передайте -g.

Вы также можете присоединить отладчик к процессу создания образа в машинном коде, который на самом деле является приложением Java. Используйте параметр --debug-attach[=< port >], чтобы запустить построитель в режиме отладки и подключить к нему свой любимый отладчик IDE, установить точки останова и т. Д. Например, установка точки останова в инициализаторе класса часто может показать трассировку стека, откуда инициализируется класс. Для получения дополнительной информации перейдите в Документацию по функциям отладочной информации. Также проверьте как включить дополнительные проверки в сгенерированном образе, чтобы помочь с отладкой.

Отслеживание памяти

Если вы считаете, что можете оптимизировать свое приложение при запуске в качестве образа в машинном коде, настроив его конфигурацию памяти, вы можете включить некоторые отладочные данные во время выполнения. Например, следующие параметры будут печатать журналы сборки мусора: XX:+PrintGC -XX:+VerboseGC.

Native Image предоставляет различные реализации сборщика мусора (GC) для управления кучей Java. См. Руководство Управление памятью во время выполнения образа.

Выводы

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

Помните, что если совет в этом посте не приближает вас к успешному использованию Native Image, вы всегда можете обратиться за помощью в # native-image open slack channel или отправить проблему на GitHub. Этот одностраничный обзор основных параметров для native-image также может оказаться полезным и напомнить о наиболее удобных командах.

GraalVM Native Image постоянно совершенствует технологии как в плане технических возможностей, так и в плане опыта разработчиков, поэтому мы ценим все отзывы, вопросы и отчеты!

Авторы Ольга Гупало и Олег Шелаев