В продакшене после прогрева JIT-компиляция новых методов прекращается, хотя процессор не загружен. Какой ресурс JVM следует проверить первым?
В первую очередь следует проверить Code Cache — область памяти JVM, где хранится скомпилированный машинный код, служебные заглушки и связанные структуры. Если Code Cache заполнен и сборщик не может освободить достаточно устаревшего кода, JIT прекращает новые компиляции, даже при свободном процессоре.
Уже скомпилированные методы обычно продолжают выполняться. Проблема проявляется в том, что новые или повторно профилируемые методы остаются интерпретируемыми либо выполняются на менее оптимизированном уровне.
JIT-компиляция появилась как компромисс между переносимостью интерпретатора и производительностью машинного кода. JVM может сначала выполнять байткод без предварительной компиляции, а затем компилировать часто используемые участки во время работы приложения.
Такой машинный код нельзя хранить в обычной Java-куче: он должен находиться в специально управляемой области, обычно с особыми правами доступа для исполняемой памяти. Поэтому у JIT есть отдельный ограниченный ресурс — Code Cache.
Компиляция требует не только процессорного времени. Для каждого скомпилированного метода нужно место под машинный код, метаданные, таблицы переходов, точки деоптимизации и служебные заглушки.
Если Code Cache переполнен, JVM может временно или полностью остановить дальнейшую JIT-компиляцию. Это приводит к падению производительности после прогрева, росту времени выполнения горячих участков и иногда к нестабильным задержкам. Свободный CPU не устраняет проблему, потому что ограничен не вычислительный ресурс, а память для размещения результата компиляции.
HotSpot обычно проходит несколько уровней выполнения: интерпретацию, быструю компиляцию и более агрессивную оптимизацию. Результаты этих компиляций представлены объектами машинного кода, часто называемыми nmethod. В Code Cache также размещаются адаптеры, stub-код и другие элементы, необходимые для вызовов JVM.
При заполнении области JVM сначала пытается вернуть место. Специальный механизм очистки ищет устаревший машинный код: например, код, который больше не используется после деоптимизации или стал недостижимым с точки зрения выполняющихся переходов. Удалить код немедленно нельзя, если поток ещё может находиться внутри него, поэтому освобождение связано с безопасным моментом и жизненным циклом скомпилированных методов.
Если освобождённого места недостаточно, компилятор может перестать принимать новые задачи. В современных реализациях HotSpot Code Cache может быть разделён на сегменты для разных типов скомпилированного кода, поэтому важно смотреть не только общий объём, но и заполнение конкретных сегментов.
Диагностика должна включать сообщения JVM о заполнении Code Cache и отключении компилятора, сведения о количестве и размере скомпилированных методов, а также данные профилировщика или JFR о компиляциях. Полезно отдельно проверить генерацию большого количества классов и методов: прокси, динамические языки, шаблоны, ORM, сериализацию и частую смену форм горячего кода.
Увеличение Code Cache часто помогает, но не является универсальным лечением. Оно повышает потребление нативной памяти и может только отложить проблему, если приложение бесконтрольно создаёт новые варианты кода. Кроме того, размер Code Cache не следует путать с размером Java heap: изменение одного ресурса не обязано менять другой.
Важно отличать остановку новых компиляций от деоптимизации. Переполнение Code Cache не означает, что все ранее скомпилированные методы немедленно вернулись к интерпретации. Уже размещённый и валидный машинный код может продолжать работать, пока JVM не признает его устаревшим или не выполнит деоптимизацию по другой причине.
Сервис использовал динамические прокси и регулярно создавал новые варианты обработчиков. После длительного прогрева в диагностических данных появилась остановка компилятора, а новые горячие методы стали дольше оставаться на интерпретируемом уровне. Загрузка CPU при этом оставалась умеренной.
Рассматривались три варианта:
Выбранным решением стало сначала подтвердить заполнение Code Cache по диагностике JVM, затем уменьшить число динамически создаваемых вариантов и только после этого увеличить допустимый объём области с учётом лимита памяти контейнера. Такой порядок предпочтительнее, потому что сохраняет контроль над причиной, а не только отодвигает момент отказа компилятора.
Нет, это зависит от реализации JVM, свободного места в отдельных сегментах и возможности очистить устаревший код. Обычно проблема выражается в невозможности принять новые задачи компиляции или в отключении компилятора после неудачных попыток освобождения. Уже скомпилированный код не обязан исчезнуть и продолжает использоваться, пока остаётся действительным.
Обычная сборка heap не освобождает машинный код напрямую. Code Cache управляется отдельными механизмами JVM, связанными с использованием скомпилированных методов, деоптимизацией и очисткой устаревших участков. Сборка heap может косвенно изменить активность приложения и жизненный цикл классов, но рассчитывать на неё как на способ очистки Code Cache нельзя.
Потому что причиной может быть не только общий объём скомпилированного кода. Приложение может генерировать слишком много уникальных методов, быстро инвалидировать ранее скомпилированные версии или упираться в конкретный сегмент Code Cache. Кроме того, увеличение области расходует нативную память и при жёстком лимите контейнера может усилить общий дефицит памяти. Поэтому размер следует менять только вместе с анализом профиля компиляций, генерации классов и потребления памяти процессом.