Статьи

JIT-компилятор, анализ встраивания и Escape

Точное время (JIT)

Компилятор Just-in-time (JIT) — это мозг виртуальной машины Java. Ничто в JVM не влияет на производительность больше, чем JIT-компилятор.

На мгновение вернемся назад и посмотрим примеры скомпилированных и не скомпилированных языков.

Такие языки, как Go, C и C ++, называются скомпилированными языками, потому что их программы распространяются в виде двоичного (скомпилированного) кода, который предназначен для конкретного процессора.

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

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

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

Чем больше раз JVM выполняет определенный фрагмент кода, тем больше информации о нем. Это позволяет JVM принимать разумные / оптимизированные решения и компилировать небольшой горячий код в двоичный файл для конкретного процессора. Этот процесс называется компиляцией Just in time (JIT) .

Теперь давайте запустим небольшую программу и посмотрим компиляцию JIT.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class App {
  public static void main(String[] args) {
    long sumOfEvens = 0;
    for(int i = 0; i < 100000; i++) {
      if(isEven(i)) {
        sumOfEvens += i;
      }
    }
    System.out.println(sumOfEvens);
  }
 
  public static boolean isEven(int number) {
    return number % 2 == 0;
  }
}
 
 
#### Run
javac App.java && \
java -server \
     -XX:-TieredCompilation \
     -XX:+PrintCompilation \
              - XX:CompileThreshold=100000 App
 
 
#### Output
87    1             App::isEven (16 bytes)
2499950000

Вывод говорит нам, что метод isEven скомпилирован. Я намеренно отключил TieredCompilation, чтобы получить только наиболее часто скомпилированный код.

JIT-скомпилированный код значительно повысит производительность вашего приложения. Хотите это проверить? Напишите простой тестовый код.

Встраивание

Встраивание — одна из самых важных оптимизаций, которые делает JIT-компилятор. Встраивание заменяет вызов метода телом метода, чтобы избежать накладных расходов при вызове метода.

Давайте снова запустим ту же программу и на этот раз понаблюдаем за вставкой.

01
02
03
04
05
06
07
08
09
10
#### Run
javac App.java && \
java -server \
     -XX:+UnlockDiagnosticVMOptions \
     -XX:+PrintInlining \
     -XX:-TieredCompilation App
 
#### Output
@ 12   App::isEven (16 bytes)   inline (hot)
2499950000

Повторная вставка значительно повысит производительность вашего приложения.

Анализ побега

Escape-анализ — это метод, с помощью которого JIT-компилятор может анализировать область использования нового объекта и решать, размещать ли его в куче Java или в стеке методов. Это также устраняет блокировки для всех не глобально экранирующих объектов

Запустим небольшую программу и понаблюдаем за сборкой мусора.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class App {
  public static void main(String[] args) {
    long sumOfArea = 0;
    for(int i = 0; i < 10000000; i++) {
      Rectangle rect = new Rectangle(i+5, i+10);
      sumOfArea += rect.getArea();
    }
    System.out.println(sumOfArea);
  }
 
  static class Rectangle {
    private int height;
    private int width;
 
    public Rectangle(int height, int width) {
      this.height = height;
      this.width = width;
    }
 
    public int getArea() {
      return height * width;
    }
  }
}

В этом примере объекты Rectangle создаются и доступны только внутри цикла, они характеризуются как NoEscape и будут размещаться в стеке, а не в куче. В частности, это означает, что сборка мусора не произойдет.

Давайте запустим программу без EscapeAnalysis.

01
02
03
04
05
06
07
08
09
10
11
#### Run
javac App.java && \
java -server \
     -verbose:gc \
     -XX:-DoEscapeAnalysis App
 
#### Output
[GC (Allocation Failure)  65536K->472K(251392K), 0.0007449 secs]
[GC (Allocation Failure)  66008K->440K(251392K), 0.0008727 secs]
[GC (Allocation Failure)  65976K->424K(251392K), 0.0005484 secs]
16818403770368

Как вы можете видеть, GC вступил в игру. Ошибка распределения означает, что у молодого поколения больше нет места для размещения объектов. Таким образом, это нормальная причина молодого GC.

На этот раз давайте запустим его с EscapeAnalysis.

1
2
3
4
5
6
7
8
#### Run
javac App.java && \
java -server \
    -verbose:gc \
    -XX:+DoEscapeAnalysis App
 
#### Output
16818403770368

На этот раз GC не произошло. Что в основном означает создание недолговечных и узкообъемных объектов, не обязательно представляет мусор

Опция DoEscapeAnalysis включена по умолчанию. Обратите внимание, что только виртуальная машина Java HotSpot Server поддерживает эту опцию.

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

Ссылка: JIT Compiler, Inlining and Escape Analysis от нашего партнера JCG Артура Мкртчяна в блоге Java Advent Calendar .