Перейти к основному содержимому

Встраивание методов в JVM

· 6 мин. чтения

1. Введение

В этом руководстве мы рассмотрим, что такое встраивание методов в виртуальной машине Java и как оно работает.

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

2. Что такое встраивание методов?

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

Хотя и требуется компиляция, она выполняется не традиционным компилятором javac , а самой JVM. Точнее, за это отвечает компилятор Just-In-Time (JIT) , который является частью JVM; javac создает только байт-код и позволяет JIT творить чудеса и оптимизировать исходный код.

Одним из наиболее важных следствий такого подхода является то, что если мы компилируем код с использованием старой Java, то же самое . class файл будет быстрее на новых JVM. Таким образом, нам не нужно перекомпилировать исходный код, а только обновить Java.

3. Как это работает JIT?

По сути, JIT-компилятор пытается встроить методы, которые мы часто вызываем, чтобы избежать накладных расходов, связанных с вызовом метода . При принятии решения о том, следует ли встраивать метод или нет, учитываются две вещи.

Во-первых, он использует счетчики для отслеживания того, сколько раз мы вызываем метод. Когда метод вызывается более определенного количества раз, он становится «горячим». Этот порог по умолчанию установлен на 10 000, но мы можем настроить его с помощью флага JVM во время запуска Java. Мы определенно не хотим встраивать все, так как это займет много времени и создаст огромный байт-код.

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

Кроме того, «горячий» метод не гарантирует, что метод будет встроен. Если он слишком большой, JIT не встроит его. Приемлемый размер ограничен флагом -XX:FreqInlineSize= , который указывает максимальное количество инструкций байт-кода для встроенного метода.

Тем не менее, настоятельно рекомендуется не изменять значение этого флага по умолчанию, если мы не уверены в том, какое влияние он может оказать. Значение по умолчанию зависит от платформы — для 64-битной версии Linux это 325.

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

4. Поиск горячих методов

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

-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining

Первый флаг регистрируется, когда происходит JIT-компиляция. Второй флаг включает дополнительные флаги, включая -XX:+PrintInlining , которые будут печатать, какие методы и где встраиваются.

Это покажет нам встроенные методы в виде дерева. Листы аннотируются и помечаются одним из следующих вариантов:

  • встроенный (горячий) — этот метод помечен как горячий и встроенный
  • слишком большой — метод не горячий, но и сгенерированный им байт-код слишком велик, поэтому он не встроен
  • горячий метод слишком большой — это горячий метод, но он не встроен, так как байт-код слишком велик

Следует обратить внимание на третье значение и попробовать оптимизировать методы с пометкой «горячий метод слишком большой».

Как правило, если мы находим горячий метод с очень сложным условным оператором, мы должны попытаться отделить содержимое оператора if- и увеличить степень детализации, чтобы JIT мог оптимизировать код. То же самое касается операторов switch и for- loop .

Мы можем сделать вывод, что ручное встраивание методов — это то, что нам не нужно делать для оптимизации нашего кода. JVM делает это более эффективно, и мы, возможно, сделали бы код длинным и трудным для понимания.

4.1. Пример

Давайте теперь посмотрим, как мы можем проверить это на практике. Сначала мы создадим простой класс, который вычисляет сумму первых N последовательных положительных целых чисел:

public class ConsecutiveNumbersSum {

private long totalSum;
private int totalNumbers;

public ConsecutiveNumbersSum(int totalNumbers) {
this.totalNumbers = totalNumbers;
}

public long getTotalSum() {
totalSum = 0;
for (int i = 0; i < totalNumbers; i++) {
totalSum += i;
}
return totalSum;
}
}

Далее простой метод будет использовать класс для выполнения вычислений:

private static long calculateSum(int n) {
return new ConsecutiveNumbersSum(n).getTotalSum();
}

Наконец, мы вызовем метод разное количество раз и посмотрим, что произойдет:

for (int i = 1; i < NUMBERS_OF_ITERATIONS; i++) {
calculateSum(i);
}

В первом запуске мы собираемся запустить его 1000 раз (меньше порогового значения 10 000, упомянутого выше). Если мы будем искать вывод метода calculateSum() , мы его не найдем. Это ожидаемо, поскольку мы не вызывали его достаточное количество раз.

Если теперь мы изменим количество итераций на 15 000 и снова проведем поиск, мы увидим:

664 262 % com.foreach.inlining.InliningExample::main @ 2 (21 bytes)
@ 10 com.foreach.inlining.InliningExample::calculateSum (12 bytes) inline (hot)

Мы видим, что на этот раз метод выполняет условия для встраивания, и JVM встраивает его.

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

-XX:FreqInlineSize=10

Как видно из предыдущего вывода, размер нашего метода составляет 12 байт. Флаг -XX: FreqInlineSize ограничивает размер метода, подходящего для встраивания, 10 байтами. Следовательно, на этот раз встраивания не должно происходить. И действительно, мы можем подтвердить это, еще раз взглянув на вывод:

330 266 % com.foreach.inlining.InliningExample::main @ 2 (21 bytes)
@ 10 com.foreach.inlining.InliningExample::calculateSum (12 bytes) hot method too big

Хотя мы изменили значение флага здесь для иллюстрации, мы должны подчеркнуть рекомендацию не изменять значение по умолчанию флага -XX:FreqInlineSize без крайней необходимости.

5. Вывод

В этой статье мы увидели, что такое встраивание методов в JVM и как это делает JIT. Мы описали, как мы можем проверить, подходят ли наши методы для встраивания или нет, и предложили, как использовать эту информацию, пытаясь уменьшить размер часто вызываемых длинных методов, которые слишком велики для встраивания.

Наконец, мы проиллюстрировали, как мы можем идентифицировать горячий метод на практике.

Все фрагменты кода, упомянутые в статье, можно найти в нашем репозитории GitHub .