Учебники

Виртуальная машина Java — JIT-компилятор

В этой главе мы узнаем о JIT-компиляторе и разнице между компилируемыми и интерпретируемыми языками.

Скомпилированные и интерпретированные языки

Такие языки, как C, C ++ и FORTRAN, являются скомпилированными языками. Их код поставляется в виде двоичного кода, предназначенного для базовой машины. Это означает, что высокоуровневый код компилируется в двоичный код одновременно статическим компилятором, написанным специально для базовой архитектуры. Созданный двоичный файл не будет работать на любой другой архитектуре.

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

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

Мы рассмотрим пример такой оптимизации ниже —

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

Но тогда интерпретируемые языки могут работать на любой машине, у которой есть действительный интерпретатор этого языка.

Java компилируется или интерпретируется?

Ява пыталась найти золотую середину. Поскольку JVM находится между компилятором javac и базовым оборудованием, компилятор javac (или любой другой компилятор) компилирует код Java в байт-код, который понимается JVM для конкретной платформы. Затем JVM компилирует байт-код в двоичном формате, используя компиляцию JIT (Just-in-time) по мере выполнения кода.

HotSpots

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

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

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

Давайте рассмотрим следующий код —

for(int i = 0 ; I <= 100; i++) {
   System.out.println(obj1.equals(obj2)); //two objects
}

Если этот код интерпретируется, интерпретатор будет выводить для каждой итерации, что классы obj1. Это связано с тем, что у каждого класса в Java есть метод .equals (), который расширен от класса Object и может быть переопределен. Таким образом, даже если obj1 является строкой для каждой итерации, вывод все равно будет выполнен.

С другой стороны, в действительности JVM заметит, что для каждой итерации obj1 имеет класс String и, следовательно, будет генерировать код, соответствующий методу .equals () класса String. Таким образом, поиск не потребуется, и скомпилированный код будет выполняться быстрее.

Такое поведение возможно только тогда, когда JVM знает, как ведет себя код. Таким образом, он ждет, прежде чем скомпилировать определенные разделы кода.

Ниже приведен еще один пример —

int sum = 7;
for(int i = 0 ; i <= 100; i++) {
   sum += i;
}

Интерпретатор для каждого цикла извлекает из памяти значение «sum», добавляет к нему «I» и сохраняет его в памяти. Доступ к памяти является дорогостоящей операцией и обычно занимает несколько циклов ЦП. Поскольку этот код запускается несколько раз, это HotSpot. JIT скомпилирует этот код и выполнит следующую оптимизацию.

Локальная копия «sum» будет храниться в регистре, специфичном для конкретного потока. Все операции будут выполнены со значением в регистре, и когда цикл завершится, значение будет записано обратно в память.

Что если другие переменные также обращаются к переменной? Поскольку обновления выполняются в локальной копии переменной каким-либо другим потоком, они увидят устаревшее значение. В таких случаях необходима синхронизация потоков. Самым базовым примитивом синхронизации было бы объявление ‘sum’ как volatile. Теперь, прежде чем получить доступ к переменной, поток сбрасывает свои локальные регистры и извлекает значение из памяти. После доступа к нему значение сразу записывается в память.

Ниже приведены некоторые общие оптимизации, которые выполняются компиляторами JIT.