JIT-компиляция в Java

| August 10, 2022

Как выглядит работа программиста при разработке Java проекта? Примерно, следующим образом:

Рисунок 1. Стандартный подход

Разработчик пишет программу в специальных файлах с расширением *.java. Затем, комплириует проект с помощью компилятора javac, который, в свою очередь переводит написанные программистом файлы с кодом в файлы с расширением *.class. *.class файлы содержат внутри так называемый байт-код, некоторое представление программы, написанное на специальном языке, которое может быть прочитано и запущено самой вирутальной машиной Java (JVM).

И это, вообщем-то, всё. Разработчик пишет код, запускает приложение на JVM и пытается с ним как-то дальше работать. Но мало кто понимает, что происходит с приложением, запущенным в JVM.

А ведь не только разработчик может сделать так, чтобы его код работал быстро, но и сама JVM содержит кладезь инструментов, помогающих программе исполняться на JVM быстрее. Один из таких значимых инструментов разывается Just-in-time компиляция или просто - JIT.

Основная часть

Что такое JIT-компиляция?

Just-in-time или JIT компиляция - это оптимизация, которую сама JVM применяет по отношению к коду разработчика.

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

Машинный код - это система команд конкретной вычислительной машины, которая интерпретируется непосредственно процессором или микропрограммами этой вычислительной машины.

Так вот, JIT-компиляция - это методика оптимизации выполнения кода, основанная на вычислении наиболее исполняемых участков кода Java программы и компиляции их в машинный код.

Теперь, зная это, не сложно представить, как будет выглядеть полная схема работы над Java программой:

Рисунок 2. JIT-компиляция

Какой код компилируется JIT-компилятором?

В определении JIT-компиляции данном выше, было сказано о “вычислении наиболее исполняемых участков кода Java программы”. Что это значит?

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

Основной единицей кодовой базы, с которой работает JIT-компиляция является - метод. Внутри JVM, при запуске Java программы создаётся некоторая таблица, в которой ведётся счёт количества вызовов каждого из методов. Буквально:

МетодСчётчик вызовов
RegistrationService#register()17
RegistrationRepository#exists()5294

Чем больше раз был вызван метод, тем больше он попадает под определение “горячих методов” (hot methods) и, соответвенно, выше шанс быть скомпилированным в машинное представление JIT-компилятором.

Стоимость JIT-компиляции

У всего есть цена, и у JIT-компиляции в том числе. JIT-компиляция происходит в runtime, а это значит она использует ресурсы JVM, которые JVM могла бы потратить на работу Вашего приложения.

Если говорить точнее, само профилирование “съедает” ресурсы JVM и непосредственно компиляция в нативный код.

Альтернативы

Главной альтернативой подходу с JIT-компиляцией является Ahead-of-Time-компиляция или просто - AOT.

AOT-компиляция активно используется в таких языках, как C и C++.

Одним из преимуществ JIT-компиляции над AOT-подходом является оптимизация кода на основе его поведения. Это явление так же известно как Profile Guided Optimization (PGO).

AOT-компиляция существует и в Java, но использовать её, на данный момент, можно только со специализированными JVM. Например, с GraalVM и её технологией - Native Image. Подход может быть полезен, если Вашему приложению требуется стартовать как можно быстрее, и сохранять наименьшее потребление памяти в ходе выполнения. Пример: бессерверные приложения.

Как устроена JIT-компиляция в HotSpot

HotSpot - это проприетарная виртуальная машина Java, разрабатываемая корпорацией Oracle.

VisualVM

Начнём издалека, а именно с VisualVM. Абсолютно каждое java приложение, запущенное в HotSpot, подвержено профилированию и компиляции в нативный код. И время, которое JVM тратит на JIT-компиляцию можно увидеть прямо в VisualVM (с плагином - VisualGC). Там же, можно увидеть влияние компиляции на остальные метрики JVM.

Рисунок 3. Время компиляции в runtime (выделено зелёным)

Клиент и Сервер

HotSpot содержит два отдельных компилятора:

  • C1 - клиентский компилятор
  • C2 - серверный компилятор

Смысл C1 заключается в скором старте и быстрой работе за счёт использования простых оптимизаций. Его назначение - усорение запуска java программы.

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

Необходимость использования двух компиляторов объясняется чисто историческими причинами. 20+ лет назад настольные компьютеры пользователей были слабыми, и были ограничены произовдительностью CPU. Именно поэтому, крайне нежелательно было замедлять запуск приложения, “какими-то там оптимизациями”. Так появился клиентский компилятор.

Современные JVM организуют работу обоих компиляторов в многоуровневую компиляцию (tiered compilation). Многоуровневой компиляцией можно управлять с помощью следующих JVM флагов:

  • -XX:+TieredCompilation
  • -XX:-TieredCompilation

Заключительная часть

В этой статье, мы познакомились с понятием - JIT-компиляция и тем, как устроена JVM для организации её работы.

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

Список материалов