После смены профиля вызовов ранее быстрый метод внезапно теряет производительность. Объясните механизм JVM, который может вызвать такой эффект.
Такой эффект может вызвать деоптимизация JIT-кода. JIT-компилятор строит машинный код на основе наблюдавшихся условий, но при нарушении одного из предположений возвращает выполнение к корректному менее оптимизированному представлению метода, обычно к интерпретатору или другой версии скомпилированного кода.
После этого JVM может собрать новый профиль и повторно скомпилировать метод. Поэтому провал производительности иногда является временным, но при постоянно меняющемся профиле вызовов возможны регулярные деоптимизации.
Интерпретатор позволяет JVM запускать Java-программы без предварительной компиляции всего приложения, но обычно уступает оптимизированному машинному коду по производительности. Поэтому HotSpot использует адаптивную компиляцию: сначала наблюдает фактическое выполнение, затем оптимизирует наиболее горячие участки.
Для более агрессивных оптимизаций JVM применяет сведения из профиля выполнения. Это решает проблему динамического выбора оптимизаций, но создаёт необходимость корректно отменять оптимизацию, если наблюдавшаяся ранее ситуация изменилась.
JIT может предположить, что в виртуальный вызов обычно передаётся объект одного конкретного класса, ветка условия почти всегда имеет один результат или проверка диапазона позволяет исключить часть проверок. Такие предположения ускоряют типичный путь выполнения.
Если профиль вызовов меняется, предположение может стать неверным. Продолжение исполнения оптимизированного кода было бы некорректным, поэтому JVM должна безопасно перейти к представлению, которое обрабатывает общий случай. Ошибочная диагностика может привести к выводу, что сборщик мусора или операционная система внезапно замедлили приложение, хотя причиной является изменение профиля и деоптимизация.
При компиляции JIT вставляет в машинный код защитные проверки и специальные точки выхода, называемые механизмами деоптимизации. Например, код может быть оптимизирован под часто встречающийся тип объекта. Если во время выполнения появляется другой тип, срабатывает проверка предположения.
JVM восстанавливает логическое состояние Java-программы: значения локальных переменных, состояние стека и позицию выполнения. Это состояние должно соответствовать безопасной точке исходного метода. Затем выполнение продолжается в интерпретаторе или менее специализированной скомпилированной версии.
Деоптимизация не означает ошибку приложения и обычно не нарушает его семантику. Она означает, что конкретная оптимизация больше не подходит для текущего профиля. После дальнейшего профилирования JVM может снова скомпилировать метод, уже учитывая новые типы или частоты ветвлений.
Практическое последствие — возможны кратковременные задержки на деоптимизации и последующую компиляцию. Если приложение постоянно чередует разные профили, оптимизированный код может регулярно становиться неактуальным; тогда усреднённая производительность и задержки могут быть хуже, чем при стабильном профиле.
Для диагностики сопоставляют изменение задержек с событиями deoptimization, компиляцией и изменением типов на горячих вызовах. Одного факта наличия JIT-компиляции недостаточно: важно понять, сохраняется ли применимость выполненных оптимизаций.
Сервис долго обрабатывал однотипные сообщения, и горячий обработчик был оптимизирован под небольшой набор реализаций интерфейса. После включения нового формата сообщений число реализаций выросло, задержка обработчика скачкообразно увеличилась, а затем частично вернулась к прежнему уровню.
Рассматривались три варианта:
Выбрали третий вариант. После подтверждения причины стабилизировали набор реализаций на горячем пути и вынесли редко встречающиеся форматы в отдельную ветку обработки. Это уменьшило число нарушений предположений JIT и сделало задержки предсказуемее.
Нет. JVM может передать выполнение интерпретатору, менее оптимизированному машинному коду или другой версии метода — конкретный путь зависит от реализации JVM и текущего состояния компиляции. Существенно то, что выполнение покидает код, чьи предположения больше нельзя безопасно использовать.
При корректной реализации JVM — нет. Деоптимизация должна сохранить наблюдаемую семантику Java-программы. Перед переходом JVM восстанавливает состояние, эквивалентное продолжению исходного метода без применённой недействительной оптимизации.
После деоптимизации JVM может продолжить собирать профиль и повторно скомпилировать метод. Если новый профиль стабилен, следующая версия кода может хорошо соответствовать реальным данным. Если же типы, ветвления или другие свойства постоянно меняются, специализация быстро теряет актуальность, и повторные компиляции не дают устойчивого выигрыша.