«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, которые будут печатать, какие методы и где встраиваются.
Это покажет нам встроенные методы в виде дерева. Листья аннотируются и помечаются одним из следующих вариантов:
-
встроенный (горячий) — этот метод помечен как горячий и встроен слишком большой — метод не горячий, но и сгенерированный им байт-код слишком большой, так что это не встроенный горячий метод too big — это горячий метод, но он не встроен, так как байт-код слишком велик
Мы должны обратить внимание на третье значение и попытаться оптимизировать методы с помощью ярлык «Горячий метод слишком большой».
Как правило, если мы находим горячий метод с очень сложным условным оператором, мы должны попытаться отделить содержимое оператора 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.baeldung.inlining.InliningExample::main @ 2 (21 bytes)
@ 10 com.baeldung.inlining.InliningExample::calculateSum (12 bytes) inline (hot)
Мы видим, что на этот раз метод удовлетворяет условиям для встраивания, и JVM встраивает его.
Стоит еще раз отметить, что если метод слишком большой, JIT не будет его встраивать, независимо от количества итераций. Мы можем проверить это, добавив еще один флаг при запуске приложения:
-XX:FreqInlineSize=10
Как видно из предыдущего вывода, размер нашего метода составляет 12 байт. Флаг -XX:FreqInlineSize ограничит размер метода, подходящего для встраивания, 10 байтами. Следовательно, на этот раз встраивания не должно происходить. И действительно, мы можем подтвердить это, еще раз взглянув на вывод:
330 266 % com.baeldung.inlining.InliningExample::main @ 2 (21 bytes)
@ 10 com.baeldung.inlining.InliningExample::calculateSum (12 bytes) hot method too big
Хотя мы изменили значение флага здесь для иллюстрации, мы должны подчеркнуть рекомендацию не изменять значение по умолчанию для -XX: Флаг FreqInlineSize, если в этом нет крайней необходимости.
5. Заключение
В этой статье мы увидели, что такое встраивание методов в JVM и как это делает JIT. Мы описали, как мы можем проверить, подходят ли наши методы для встраивания или нет, и предложили, как использовать эту информацию, пытаясь уменьшить размер часто вызываемых длинных методов, которые слишком велики для встраивания.
Наконец, мы продемонстрировали, как на практике можно идентифицировать горячий метод.
Все фрагменты кода, упомянутые в статье, можно найти в нашем репозитории GitHub.