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

JEP 483: Ahead-of-Time Class Loading & Linking

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

AuthorsIoi Lam, Dan Heidinga, & John Rose
ОтветственныйIoi Lam
ТипFeature
ОбластьJDK
СтатусClosed / Delivered
Выпуск24
Компонентhotspot / runtime
Обсуждениеleyden dash dev at openjdk dot org
Связан сJEP 515: Ahead-of-Time Method Profiling
JEP 514: Ahead-of-Time Command-Line Ergonomics
JEP 544: Ahead-of-Time Code Compilation
РецензентыAlex Buckley, Brian Goetz, Mark Reinhold, Vladimir Kozlov
ОдобренVladimir Kozlov
Создан2023/09/06 04:07
Обновлён2026/08/26 18:21
Задача8315737

Аннотация

Сократить время запуска: классы приложения должны быть доступны сразу, в загруженном и скомпонованном состоянии, когда стартует HotSpot Java Virtual Machine. Для этого приложение отслеживается во время одного прогона, а загруженные и скомпонованные формы всех классов сохраняются в кэше для последующих прогонов. Заложить основу для дальнейшего сокращения времени запуска и времени прогрева.

Цели

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

  • Не требовать никаких изменений в коде приложений, библиотек или фреймворков.

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

  • Не требовать использования инструментов jlink или jpackage.

  • Заложить основу для дальнейшего сокращения времени запуска, а также времени прогрева, т. е. времени, за которое HotSpot JVM оптимизирует код приложения до пиковой производительности.

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

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

Мотивация

Платформа Java в высшей степени динамична. В этом её большая сила.

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

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

Однако за весь этот динамизм приходится платить, причём при каждом запуске приложения.

Во время запуска типичного серверного приложения HotSpot JVM выполняет большой объём работы, чередуя несколько видов действий:

Если, кроме того, приложение использует фреймворк, например Spring Framework, то при запуске фреймворк ищет аннотации @Bean, @Configuration и связанные с ними, и работы становится ещё больше.

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

Однако приложения склонны повторяться: при каждом запуске они часто делают по сути одно и то же. Они сканируют одни и те же JAR-файлы, считывают, разбирают, загружают и компонуют одни и те же классы, выполняют одни и те же статические инициализаторы и с помощью рефлексии настраивают одни и те же объекты приложения. Чтобы сократить время запуска, главное — постараться выполнить хотя бы часть этой работы энергично, заранее, а не точно в срок. Иначе говоря, в терминах проекта Leyden, мы стремимся сдвинуть часть этой работы на более раннее время.

Описание

Мы добавляем в HotSpot JVM поддержку AOT Cache (кэш, подготовленный заранее), в котором можно хранить классы после их считывания, разбора, загрузки и компоновки. Кэш, созданный для конкретного приложения, можно повторно использовать в последующих прогонах этого приложения, чтобы сократить время запуска.

Кэш создаётся в два шага. Сначала запустите приложение один раз, в обучающем прогоне, чтобы записать его AOT-конфигурацию, в данном случае в файл app.aotconf:

$ java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf \
       -cp app.jar com.example.App ...

Затем создайте по этой конфигурации кэш в файле app.aot:

$ java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf \
       -XX:AOTCache=app.aot -cp app.jar

Второй шаг не запускает приложение, а только создаёт кэш. (Мы намерены упростить создание кэша в дальнейшей работе.) Формат файлов конфигурации и кэша не специфицирован и может измениться без предупреждения.

После этого при тестировании или в рабочей среде запускайте приложение с кэшем:

$ java -XX:AOTCache=app.aot -cp app.jar com.example.App ...

(Если файл кэша непригоден или не существует, HotSpot выводит предупреждение и продолжает работу.)

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

Например, вот программа, которая, несмотря на краткость, использует Stream API и поэтому вызывает считывание, разбор, загрузку и компоновку почти 600 классов JDK:

import java.util.*;
import java.util.stream.*;

public class HelloStream {

    public static void main(String ... args) {
        var words = List.of("hello", "fuzzy", "world");
        var greeting = words.stream()
            .filter(w -> !w.contains("z"))
            .collect(Collectors.joining(", "));
        System.out.println(greeting);  // hello, world
    }

}

На JDK 23 эта программа выполняется за 0,031 секунды. После небольшой дополнительной работы по созданию AOT-кэша на JDK 24 она выполняется за 0,018 секунды — улучшение на 42 %. AOT-кэш занимает 11,4 мегабайта.

В качестве типичного серверного приложения рассмотрим Spring PetClinic версии 3.2.0. При запуске оно загружает и компонует около 21 000 классов. На JDK 23 оно запускается за 4,486 секунды, а на JDK 24 с AOT-кэшем за 2,604 секунды — по совпадению, тоже улучшение на 42 %. AOT-кэш занимает 130 мегабайт.

Как обучить свою JVM

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

Один из способов добиться этого — добавить в приложение второй главный класс специально для обучения, например com.example.AppTrainer. Этот класс может вызывать рабочий главный класс, чтобы задействовать основные режимы работы приложения, используя временный каталог для файлов журналов, локальную сетевую конфигурацию и, при необходимости, имитацию базы данных. Возможно, такой главный класс у вас уже есть в виде интеграционного теста.

Ещё несколько советов:

  • Чтобы оптимизировать время запуска, выстройте обучающий прогон так, чтобы он загружал те же классы, которые загружает рабочий прогон при запуске. Проверить, какие классы загружаются, можно с помощью параметра командной строки -verbose:class или события jdk.ClassLoad в JDK Flight Recorder.

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

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

  • Сосредоточьтесь на широком наборе коротких проверочных сценариев, которые иногда называют «дымовыми тестами» или «тестами работоспособности». Часто этого достаточно, чтобы загрузить большинство классов, которые понадобятся в рабочей среде. Избегайте больших наборов тестов, которые покрывают редкие граничные случаи и редко используемую функциональность. Избегайте также нагрузочных и регрессионных тестов: как правило, они не отражают типичных действий при запуске.

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

Согласованность обучающего и последующих прогонов

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

  • Все прогоны должны использовать один и тот же выпуск JDK и выполняться на одной и той же аппаратной архитектуре (например, x64 или aarch64) и операционной системе.

  • У всех прогонов должны быть согласованные пути классов. В последующем прогоне можно указать дополнительные элементы пути классов, добавленные в конец обучающего пути классов; в остальном пути классов должны быть идентичны. Пути классов должны содержать только JAR-файлы; каталоги в путях классов не поддерживаются, потому что HotSpot не может эффективно проверять их согласованность.

  • У всех прогонов должны быть согласованные параметры модулей в командной строке и согласованные графы модулей. Аргументы параметров -m, --module, -p, --module-path, --add-modules и --enable-native-access, если они указаны, должны быть идентичны. Параметры --add-exports, --add-opens, --add-reads, --illegal-native-access, --limit-modules, --patch-module и --upgrade-module-path использовать нельзя.

  • Ни в одном прогоне нельзя использовать агенты JVMTI, которые могут произвольно переписывать class-файлы с помощью события JVMTI ClassFileLoadHook или вызывают функции JVMTI AddToBootstrapClassLoaderSearch или AddToSystemClassLoaderSearch.

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

Полезное исключение из требования согласованности: обучающий и последующие прогоны могут использовать разные сборщики мусора. Ещё одно полезное исключение: обучающий и последующие прогоны могут использовать разные главные классы; это даёт гибкость при построении обучающих прогонов, как отмечено выше.

Чтобы проверить, правильно ли HotSpot настроен на использование AOT-кэша, добавьте в командную строку параметр -XX:AOTMode=on:

$ java -XX:AOTCache=app.aot -XX:AOTMode=on \
       -cp app.jar com.example.App ...

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

В производственных средах -XX:AOTMode=on следует использовать с осторожностью. Ваше приложение может не запуститься, если без вашего ведома будет добавлен несовместимый параметр VM. Например, облачный провайдер может запускать ваше приложение с перехватчиком загрузки class-файлов JVMTI для мониторинга.

(Обновление: начиная с JDK 27, -XX:AOTMode=on теперь -XX:AOTMode=required.)

При необходимости AOT Cache можно полностью отключить с помощью -XX:AOTMode=off. Можно также задать режим по умолчанию с помощью -XX:AOTMode=auto. В этом случае HotSpot пытается использовать AOT-кэш, указанный параметром -XX:AOTCache; если кэш непригоден или не существует, HotSpot выдаёт предупреждение и продолжает работу.

История

Предлагаемый здесь кэш Ahead-of-Time — естественное развитие давней возможности HotSpot JVM — class-data sharing (CDS, совместное использование данных классов).

CDS впервые появился в обновлении JDK 5 в 2004 году. Изначально его целью было уменьшить объём памяти, занимаемой несколькими Java-приложениями, работающими на одной машине. Для этого CDS читал и разбирал class-файлы JDK и сохранял полученные метаданные в архивный файл только для чтения, который затем несколько процессов HotSpot могли отображать непосредственно в память, используя одни и те же страницы виртуальной памяти. Позже мы расширили CDS, чтобы он мог хранить и метаданные классов приложения.

Сегодня выгода от совместного использования в CDS уменьшилась из-за новых практик безопасности, таких как address space layout randomization (ASLR, рандомизация размещения адресного пространства), из-за которой адрес, по которому файл отображается в память, становится непредсказуемым. Однако CDS по-прежнему заметно ускоряет запуск — настолько, что сборки JDK 12 и более поздних версий содержат встроенный архив CDS с метаданными более чем тысячи часто используемых классов JDK. Поэтому CDS используется повсеместно, хотя многие Java-разработчики никогда о нём не слышали и мало кто использовал его напрямую.

AOT-кэш развивает CDS: он не только заранее читает и разбирает class-файлы, но и загружает и компонует их. Эффект двух последних оптимизаций можно увидеть, отключив их параметром -XX:-AOTClassLinking при создании кэша:

$ java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf \
       -XX:AOTCache=app.aot -XX:-AOTClassLinking

С этим параметром видно, что большая часть ускорения запуска программы HelloStream достигается за счёт загрузки и компоновки в режиме Ahead-of-Time, а большая часть ускорения запуска приложения PetClinic — за счёт заранее выполняемых чтения и разбора, которые CDS уже делает сегодня (всё время указано в секундах, проценты накопительные):

HelloStream PetClinic
JDK 23 0.031 4.486
AOT-кэш без загрузки и компоновки 0.027 (+13%) 3.008 (+33%)
AOT-кэш с загрузкой и компоновкой 0.018 (+42%) 2.604 (+42%)

Поэтому пользователи Spring Boot и, в более общем смысле, Spring Framework уже сегодня могут значительно ускорить запуск, просто используя возможность CDS, доступную в предыдущих выпусках JDK.

Новые параметры командной строки -XX:AOT* на данный момент в основном являются макросами для существующих параметров CDS, таких как -Xshare, -XX:DumpLoadedClassList и -XX:SharedArchiveFile. Мы вводим параметры -XX:AOT*, чтобы обеспечить единообразие для пользователей как этой, так и будущих возможностей Ahead-of-Time, а также чтобы отказаться от потенциально сбивающих с толку слов «share» и «shared».

Совместимость

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

Так происходит потому, что для Java-кода момент и порядок чтения, разбора, загрузки и компоновки классов не имеют значения. Спецификации языка Java и виртуальной машины дают JVM большую свободу в планировании этих операций. Когда мы переносим эти операции из режима just-in-time в режим Ahead-of-Time, приложение видит загрузку и компоновку классов так, как если бы JVM выполнила эту работу точно в запрошенный момент, — только необъяснимо быстро.

Дальнейшая работа

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

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

  • ZGC пока не поддерживается. Мы намерены устранить это ограничение в дальнейшей работе.

  • В некоторых случаях HotSpot не может загрузить классы заранее, не говоря уже о том, чтобы скомпоновать их. К ним относятся классы, загружаемые пользовательскими загрузчиками классов, старые классы, которым нужна старая версия верификатора байт-кода, и подписанные классы. Если класс не может быть загружен заранее, другие классы, которые можно загрузить заранее, не могут быть заранее скомпонованы с ним. Во всех таких случаях HotSpot, как обычно, возвращается к загрузке и компоновке в режиме just-in-time. Мы можем устранить эти ограничения в дальнейшей работе, если и когда они окажутся существенными.

  • Загрузка и компоновка классов в режиме Ahead-of-Time открывает путь к будущему сокращению времени прогрева. В будущем во время обучающих прогонов мы сможем записывать статистику о том, какой код выполняется чаще всего, и кэшировать сгенерированный оптимизированный код. Так приложения смогут сразу стартовать в оптимизированном состоянии.

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

  • Мы создадим новые модульные тесты для новых параметров командной строки.

  • Загрузка и компоновка Ahead-of-Time не зависит от существующих возможностей CDS. Большинство тестов CDS должны проходить при запуске с параметром -XX:+AOTClassLinking. Некоторые тесты чувствительны к порядку загрузки классов; мы соответствующим образом их доработаем.

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

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

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

  • Мы предполагаем, что низкоуровневые побочные эффекты загрузки и компоновки в режиме Ahead-of-Time на практике несущественны. К ним относятся момент обращений к файловой системе, сообщения журнала, внутренние служебные операции JDK и изменения в использовании процессора и памяти. Приложения, которые отслеживают такие тонкие эффекты и зависят от них, могут стать нестабильными, если классы загружаются и компонуются заранее. Мы предполагаем, что такие приложения редки и что их можно доработать, чтобы это компенсировать.