Виртуальная машина Java - JIT-компилятор
В этой главе мы узнаем о JIT-компиляторе и разнице между компилируемыми и интерпретируемыми языками.
Скомпилированные и интерпретируемые языки
Такие языки, как C, C ++ и FORTRAN, являются компилируемыми языками. Их код доставляется в виде двоичного кода, предназначенного для базовой машины. Это означает, что код высокого уровня сразу же компилируется в двоичный код статическим компилятором, написанным специально для базовой архитектуры. Созданный двоичный файл не будет работать ни на какой другой архитектуре.
С другой стороны, интерпретируемые языки, такие как Python и Perl, могут работать на любой машине, если у них есть действующий интерпретатор. Он проходит построчно по высокоуровневому коду, преобразовывая его в двоичный код.
Интерпретируемый код обычно медленнее, чем скомпилированный. Например, рассмотрим петлю. Интерпретируемый преобразует соответствующий код для каждой итерации цикла. С другой стороны, скомпилированный код сделает перевод только одним. Кроме того, поскольку интерпретаторы видят только одну строку за раз, они не могут выполнять какой-либо значительный код, например, изменяя порядок выполнения операторов, таких как компиляторы.
Мы рассмотрим пример такой оптимизации ниже -
Adding two numbers stored in memory. Поскольку доступ к памяти может потреблять несколько циклов ЦП, хороший компилятор выдаст инструкции для выборки данных из памяти и выполнит добавление только тогда, когда данные доступны. Он не будет ждать, а тем временем выполнит другие инструкции. С другой стороны, во время интерпретации такая оптимизация невозможна, поскольку интерпретатор не знает весь код в любой момент времени.
Но тогда интерпретируемые языки могут работать на любой машине, на которой есть действующий интерпретатор этого языка.
Java компилируется или интерпретируется?
Java пыталась найти золотую середину. Поскольку JVM находится между компилятором javac и базовым оборудованием, компилятор javac (или любой другой компилятор) компилирует код Java в байт-код, который понимается JVM, зависящим от платформы. Затем JVM компилирует байт-код в двоичном формате, используя JIT (Just-in-time) компиляцию, по мере выполнения кода.
Горячие точки
В типичной программе часто выполняется лишь небольшой участок кода, и часто именно этот код существенно влияет на производительность всего приложения. Такие участки кода называются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;
}
Интерпретатор для каждого цикла извлекает значение «суммы» из памяти, добавляет к нему «I» и сохраняет его обратно в память. Доступ к памяти - дорогостоящая операция и обычно занимает несколько циклов процессора. Поскольку этот код запускается несколько раз, это HotSpot. JIT скомпилирует этот код и произведет следующую оптимизацию.
Локальная копия «суммы» будет храниться в регистре, специфичном для конкретного потока. Все операции будут выполнены со значением в регистре, и когда цикл завершится, значение будет записано обратно в память.
Что, если к переменной обращаются и другие потоки? Поскольку обновления выполняются в локальной копии переменной каким-либо другим потоком, они увидят устаревшее значение. В таких случаях требуется синхронизация потоков. Самый простой примитив синхронизации - объявить сумму как изменчивую. Теперь, прежде чем обращаться к переменной, поток очищает свои локальные регистры и извлекает значение из памяти. После обращения к нему значение сразу же записывается в память.
Ниже приведены некоторые общие оптимизации, которые выполняются компиляторами JIT:
- Встраивание метода
- Устранение мертвого кода
- Эвристика для оптимизации сайтов звонков
- Постоянное сворачивание