В новом переводе от команды Spring АйО рассмотрим, как можно ускорить Java-приложения без переписываний: в свежих JDK появились Ahead-of-Time оптимизации кэша, которые выносят «дорогие» этапы загрузки/линковки классов (и даже частично профилирование методов) из рантайма в заранее подготовленный артефакт.
Рассмотрим как устроен AOT-кэш в JDK 24–26, какие есть workflow (3 шага vs 2 шага/в один прогон), где прячутся подводные камни вроде удвоения требований по памяти при -XX:AOTCacheOutput, и какие практики обучения помогут реально сократить время старта и быстрее выйти на пик производительности.
С началом нового года сосредоточьтесь на повышении производительности ваших Java-приложений, применив Ahead-of-Time (AOT) возможности кэширования, добавленные в последних релизах JDK. Эта статья проведёт вас через использование AOT-оптимизаций кэша в вашем приложении, чтобы сократить время запуска и быстрее выходить на пиковую производительность.
Что такое Ahead-of-Time кэш в JDK
В JDK 24 был представлен Ahead-Of-Time (AOT) кэш — функция HotSpot JVM, которая сохраняет классы по��ле того, как они были прочитаны, разобраны, загружены и слинкованы. Создание AOT-кэша выполняется под конкретное приложение, и затем его можно повторно использовать в последующих запусках этого же приложения, улучшая время до первой функциональной единицы работы (время запуска) и время выхода на пиковую производительность (время прогрева).
Чтобы сгенерировать AOT-кэш, нужно выполнить два шага:
Обучение (training) — запись наблюдений за приложением в работе. Запись можно включить, задав -XX:AOTMode=record и указав назначение для конфигурационного файла через -XX:AOTConfiguration:
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf \ -cp app.jar com.example.App ...
Цель этого шага — ответить на вопросы вроде «Какие классы приложение загружает и инициализирует?», «Какие методы становятся горячими?» — и сохранить результаты в конфигурационный файл (app.aotconf).
Сборка (assembly) — преобразование наблюдений из конфигурационного файла в AOT-кэш (app.aot).
java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf \ -XX:AOTCache=app.aot -cp app.jar
Чтобы получить выигрыш во времени запуска, запускайте приложение, указывая флаг -XX:AOTCache на AOT-кэш, созданный на предыдущих шагах.
java -XX:AOTCache=app.aot -cp app.jar com.example.App ...
Улучшение времени запуска достигается за счёт переноса работы, которая обычно выполняется «just in time» при запуске программы, на более ранний этап — во второй шаг, где создаётся кэш. После этого программа стартует быстрее на третьей фазе, потому что её классы сразу доступны из кэша.
Трёхшаговый процесс (train+assemble+run) стал доступен начиная с JDK 24 благодаря JEP 483: Ahead-of-Time Class Loading & Linking — первой функции, включённой в JDK на основе исследований Project Leyden. Набор бенчмарков подтверждает эффективность этой функции и других производительных улучшений Leyden, как показано на рисунке 1.

В JDK 25 изменения, внесённые JEP 515 — Ahead-of-Time Method Profiling, позволили включать в AOT-кэш профили часто выполняемых методов. Это улучшение ускоряет прогрев приложения, поскольку JIT может начинать генерацию нативного кода сразу при старте приложения. Новая AOT-возможность не требует добавлять какие-либо дополнительные ограничения на выполнение приложения — достаточно использовать уже существующие команды создания AOT-кэша. Более того, бенчмарки показали улучшение и времени запуска (рисунок 2).

В JDK 25 также упростили процесс генерации AOT-кэш��, сделав его возможным в один шаг — с помощью задания аргумента для флага -XX:AOTCacheOutput:
# Training Run + Assembly Phase java -XX:AOTCacheOutput=app.aot \ -cp app.jar com.example.App ...
При передаче -XX:AOTCacheOutput=[расположение кэша] JVM создаёт кэш при завершении работы. JEP 514 — Ahead-of-Time Command-Line Ergonomics — ввёл двухшаговый процесс создания и использования AOT-кэша.
# Training Run + Assembly Phase java -XX:AOTCacheOutput=app.aot \ -cp app.jar com.example.App ... # Deployment Run java -XX:AOTCache=app.aot -cp app.jar com.example.App ...
Двухшаговый workflow может работать не так, как ожидается, в средах с ограниченными ресурсами. Внутренний процесс, который создаёт AOT-кэш, использует собственную Java-кучу того же размера, что и куча, используемая на обучающем прогоне. В результате объём памяти, необходимы�� для завершения одношаговой генерации AOT-кэша, вдвое превышает размер кучи, заданный в командной строке. Например, если к шагу java -XX:AOTCacheOutput=... также добавлены опции -Xms2g -Xmx2g, задающие кучу 2 ГБ, то окружению потребуется 4 ГБ, чтобы завершить workflow.
Разделение шагов, как в трёхфазном workflow, может быть лучшим выбором, если вы планируете разворачивать приложение в небольших облачных тенантах. В таких случаях можно выполнить обучение на небольшом инстансе, а создание AOT-кэша — на более крупном. Так обучающий прогон будет отражать окружение развёртывания, а создание AOT-кэша сможет использовать дополнительные CPU-ядра и память большого инстанса.
Независимо от того, какой workflow вы выберете, давайте подробнее рассмотрим требования к AOT-кэшу и то, как настроить его так, чтобы он наилучшим образом отвечал потребностям вашего приложения.
Как создать AOT-кэш, который нужен вашему приложению
Тренировочный/Обучающий прогон (“Training run”) и продакшен-запуск должны давать согласованные результаты — просто в окружении развёртывания всё должно происходить быстрее. Чтобы этого добиться, фаза сборки выступает промежуточным этапом между обучающим и продакшен-запусками (рисунок 3).

Чтобы тренировочные прогоны и последующие запуски давали согласованные результаты, убедитесь, что:
Метки времени JAR-файлов сохраняются между обучающими прогонами.
В обучающих прогонах и в продакшен-запуске используется один и тот же релиз JDK для одной и той же аппаратной архитектуры и операционной системы.
Поведение приложения в обучающем прогоне похоже на ожидаемое поведение приложения в продакшене (например, наиболее активно используемые области приложения в обучающем прогоне — это те же области, которые наиболее активно используются в продакшене).
Classpath приложения задан как список JAR-файлов, без каталогов, wildcard-ов или вложенных JAR-ов.
Classpath в продакшен-запуске должен быть надмножеством classpath обучающего прогона.
Не используйте JVMTI-агенты, которые вызывают API
AddToBootstrapClassLoaderSearchиAddToSystemClassLoaderSearch.
Комментарий от Михаила Поливаха
Вот тут Анна Мария на самом деле не подчеркивает, но я считаю, что это прямо важно донести - вы не просто должны использовать ту же самую версию Java платформы при training run и в продакшене, вы должны использовать точно такой же дистрибутив JDK, т.е:
Тот же вендор
Та же OS
Та же арзитектура процессора
Иначе кеш рискует быть инвалидированым и усилия насмарку. Более того, даже другая patch версия JDK не может быть исопльзована.
То есть тут JDK становится прибит гвоздями по сути.
Чтобы проверить, корректно ли настроена JVM для использования AOT-кэша, можно добавить в командную строку опцию -XX:AOTMode=on:
java -XX:AOTCache=app.aot -XX:AOTMode=on \ -cp app.jar com.example.App ...
JVM сообщит об ошибке, если AOT-кэш не существует или если ваша конфигурация нарушает любое из требований выше. Функции, появившиеся в JDK 24 и 25, не поддерживали сборщик мусора Z Garbage Collector (ZGC). Однако это ограничение больше не действует начиная с JDK 26 — с появлением JEP 516: Ahead-of-Time Object Caching with Any GC.
Чтобы AOT-кэш эффективно работал в продакшене, обучающий прогон и все последующие запуски должны быть по сути идентичны. Обучающие прогоны — это способ наблюдать, что делает приложение в разных запусках, и обычно они бывают двух типов:
интеграционные тесты, выполняемые на этапе сборки;
продакшен-нагрузки, требующие обучения в продакшене.
Избегайте загрузки неиспользуемых классов на шаге обучения и не используйте тяжёлые тестовые фреймворки, чтобы держать AOT-кэш минимальным. В обучении можно замокать внешние зависимости, чтобы загрузить нужные классы, но учитывайте, что это может добавить лишние записи в кэш. Эффективность AOT-кэша зависит от того, насколько точно обучающий прогон соответствует поведению в продакшене. Если вы пересобираете приложение или обновляете JDK, AOT-кэш нужно сгенерировать заново.
Также важно помнить: AOT-кэш валиден только для текущего состояния приложения. Любые изменения кода, добавление библиотек, обновление существующих библиотек сделают кэш недействительным. Поэтому его придётся пересоздавать с каждой новой сборкой приложения. Иначе вы рискуете получить краши или неопределённое поведение (например, когда методы отсутствуют в кэше).
Комментарий от Михаила Поливаха
Тут 2 момента. На практике, поскольку механизм новый, то да, невалидный AotCache, в теории, может прямо крешнуть HotSpot. Такое уже было у парней из Amper Build tool:
Но! Есть важное но. На деле Project Leyden команда даже в изначальном JEP 483 поясняла, что одна из целей это иметь "fail-safe" кеш, то есть даже если Aot Cache будет закоррапчен, это не должно валить VM. Иными словами, это планируют исправлять. Это баг, а не фича.
Если вы замечаете проблемы или не получаете ожидаемого прироста производительности, попробуйте запускать приложение с -Xlog:aot,class+path=info, чтобы отслеживать, что именно оно загружает из кэша.
Советы по эффективным обучающим прогонам
Здесь есть компромисс между производительностью и тем, насколько легко организовать обучение. Использовать продакшен-запуск для обучения не всегда практично, особенно для серверных приложений, которые могут создавать лог-файлы, открывать сетевые соединения, обращаться к базам данных и т. д. В таких случаях лучше сделать синтетический обучающий прогон, максимально похожий на реальные продакшен-запуски.
Если обучающий прогон загружает те же классы, что и продакшен, это помогает оптимизировать время старта. Чтобы определить, какие классы загружает обучающий прогон, можно при запуске добавить флаг -verbose:class. Либо наблюдать за загружаемыми классами, включив JFR-событие jdk.ClassLoad и профилируя с его помощью приложение:
# настроить событие jfr configure jdk.ClassLoad#enabled=true # профилировать сразу при запуске приложения java -XX:StartFlightRecording:settings=custom.jfc,duration=60s,filename=/tmp/AOT.jfr
Когда файл записи готов, можно посмотреть, какие классы были загружены, а также какие методы приложение использует чаще всего, выполнив следующие команды jfr:
# вывести события jdk.ClassLoad из файла записи jfr print --events "jdk.ClassLoad" /tmp/AOT.jfr # посмотреть часто выполняемые методы jfr view hot-methods /tmp/AOT.jfr
Если вы обнаружите, что есть часто используемые методы, которые не были выявлены в обучающем прогоне, «прогоните» их явно. Можно отработать стандартные режимы приложения, используя временный каталог файлов, локальную сетевую конфигурацию и, при необходимости, замоканную базу данных. Избегайте загрузки неиспользуемых классов во время обучения и не используйте тяжёлые тестовые фреймворки, чтобы держать AOT-кэш минимальным. Вместо этого применяйте smoke-тесты, покрывающие типичные пути старта; избегайте обширных наборов и stress/regression-тестов.
Выводы
В заключение: чтобы создать AOT-кэш и получить прирост производительности, важно обратить внимание на следующее:
Актуальность кэша (валидность/устаревание): если вы пересобираете приложение или обновляете JDK, AOT-кэш необходимо сгенерировать заново.
Переносимость: AOT-кэш специфичен для JVM и конкретной платформы.
Покрытие стартового пути: обучающий прогон должен охватывать типичные сценарии запуска приложения. Если обучающий прогон поверхностный, прогрев будет недостаточным, и выигрыш от кэша окажется ограниченным.
Эксплуатационная схема: и JAR приложения, и AOT-кэш должны запускаться с минимально необходимыми привилегиями и соответствовать практикам неизменяемой инфраструктуры.
Производительность приложения — это непрерывная работа, потому что ПО развивается: добавляются новые функции, меняются библиотеки и фреймворки, растут нагрузки, трансформируется инфраструктура (например, переход в облако, оркестрация контейнеров и т. п.). А вместе с этими изменениями эволюционируют и ваши цели по производительности. Инвестируйте в обучение приложения уже сегодня и следите за релизами JDK, чтобы получать доступ к доступным оптимизациям: с каждым выпуском производительность становится лучше!

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
