1. Обзор
Языки программирования классифицируются на основе их уровней абстракции. Мы различаем языки высокого уровня (Java, Python, JavaScript, C++, Go), низкоуровневые (Assembler) и, наконец, машинный код.
Каждый код языка высокого уровня, такой как Java, должен быть переведен в машинный код для выполнения. Этот процесс перевода может быть компиляцией или интерпретацией. Однако есть и третий вариант. Комбинация, которая стремится использовать преимущества обоих подходов.
В этом руководстве мы рассмотрим, как код Java компилируется и выполняется на разных платформах. Мы рассмотрим некоторые особенности проектирования Java и JVM. Это поможет нам определить, компилируется ли Java, интерпретируется или является гибридом того и другого.
2. Компиляция и интерпретация
Начнем с рассмотрения некоторых основных различий между компилируемыми и интерпретируемыми языками программирования .
2.1. Компилируемые языки
Компилируемые языки (C++, Go) преобразуются непосредственно в машинный код программой-компилятором.
Они требуют явного шага сборки перед выполнением. Вот почему нам нужно перестраивать программу каждый раз, когда мы вносим изменения в код.
Компилируемые языки, как правило, быстрее и эффективнее , чем интерпретируемые языки . Однако сгенерированный ими машинный код зависит от платформы.
2.2. Интерпретируемые языки
С другой стороны, в интерпретируемых языках (Python, JavaScript) нет шагов сборки. Вместо этого интерпретаторы оперируют исходным кодом программы во время ее выполнения.
Когда-то интерпретируемые языки считались значительно медленнее компилируемых. Однако с развитием JIT-компиляции разрыв в производительности сокращается. Однако следует отметить, что компиляторы JIT превращают код из интерпретируемого языка в машинный код по мере выполнения программы.
Кроме того, мы можем выполнять код интерпретируемого языка на нескольких платформах , таких как Windows, Linux или Mac. Интерпретируемый код не имеет отношения к конкретному типу архитектуры ЦП.
3. Пишите один раз и работайте где угодно
Java и JVM были разработаны с учетом переносимости. Таким образом, большинство популярных сегодня платформ могут выполнять код Java.
Это может звучать как намек на то, что Java — чисто интерпретируемый язык. Однако перед выполнением исходный код Java необходимо скомпилировать в байт -код . Байт-код — это специальный машинный язык, встроенный в JVM .
JVM интерпретирует и выполняет этот код во время выполнения.
Именно JVM создается и настраивается для каждой платформы, поддерживающей Java, а не для наших программ или библиотек.
Современные JVM также имеют JIT-компилятор. Это означает, что JVM оптимизирует наш код во время выполнения , чтобы получить те же преимущества в производительности, что и скомпилированный язык.
4. Компилятор Java
Инструмент командной строки javac компилирует исходный код Java в файлы классов Java, содержащие независимый от платформы байт-код:
$ javac HelloWorld.java
Файлы исходного кода имеют суффиксы .java
, а файлы классов, содержащие байт-код, создаются с расширением . суффиксы класса .
5. Виртуальная машина Java
Скомпилированные файлы классов (байт-код) могут быть выполнены виртуальной машиной Java (JVM ) :
$ java HelloWorld Hello Java!
Давайте теперь более подробно рассмотрим архитектуру JVM. Наша цель — определить, как байт-код преобразуется в машинный код во время выполнения.
5.1. Обзор архитектуры
JVM состоит из пяти подсистем:
- ClassLoader
- JVM-память
- Двигатель исполнения
- Собственный интерфейс метода и
- Библиотека собственных методов
5.2. ClassLoader
JVM использует подсистемы ClassLoader для переноса скомпилированных файлов классов в память JVM .
Помимо загрузки, ClassLoader также выполняет связывание и инициализацию. Это включает:
- Проверка байт-кода на наличие нарушений безопасности
- Выделение памяти для статических переменных
- Замена символических ссылок на память исходными ссылками
- Присвоение исходных значений статическим переменным
- Выполнение всех блоков статического кода
5.3. Исполнительный механизм
Подсистема механизма выполнения отвечает за чтение байт-кода, преобразование его в машинный код и его выполнение.
За выполнение отвечают три основных компонента, включая интерпретатор и компилятор:
- Поскольку JVM не зависит от платформы, она использует интерпретатор для выполнения байт-кода.
- Компилятор JIT повышает производительность за счет компиляции байт-кода в машинный код для повторяющихся вызовов методов.
- Сборщик мусора собирает и удаляет все объекты, на которые нет ссылок .
Механизм выполнения использует интерфейс собственных методов (JNI) для вызова собственных библиотек и приложений.
5.4. Как раз вовремя Компилятор
Основным недостатком интерпретатора является то, что каждый раз, когда вызывается метод, он требует интерпретации, которая может быть медленнее, чем скомпилированный собственный код. Java использует JIT-компилятор для решения этой проблемы.
Компилятор JIT не полностью заменяет интерпретатор. Механизм выполнения все еще использует его. Однако JVM использует JIT-компилятор в зависимости от того, как часто вызывается метод.
Компилятор JIT компилирует весь байт-код метода в собственный машинный код , чтобы его можно было повторно использовать напрямую . Как и в случае со стандартным компилятором, есть генерация промежуточного кода, оптимизация, а затем создание собственного машинного кода.
Профилировщик — это специальный компонент JIT-компилятора, отвечающий за поиск горячих точек. JVM решает, какой код компилировать JIT, на основе информации о профилировании, собранной во время выполнения.
Одним из последствий этого является то, что программа Java может быстрее выполнять свою работу после нескольких циклов выполнения. Как только JVM изучит горячие точки, она сможет создать собственный код, позволяющий работать быстрее.
6. Сравнение производительности
Давайте посмотрим, как JIT-компиляция улучшает производительность Java во время выполнения.
6.1. Тест производительности Фибоначчи
Мы будем использовать простой рекурсивный метод для вычисления n-го числа Фибоначчи:
private static int fibonacci(int index) {
if (index <= 1) {
return index;
}
return fibonacci(index-1) + fibonacci(index-2);
}
Чтобы измерить выигрыш в производительности при повторных вызовах метода, мы запустим метод Фибоначчи 100 раз:
for (int i = 0; i < 100; i++) {
long startTime = System.nanoTime();
int result = fibonacci(12);
long totalTime = System.nanoTime() - startTime;
System.out.println(totalTime);
}
Во-первых, мы скомпилируем и выполним код Java в обычном режиме:
$ java Fibonacci.java
Затем мы выполним тот же код с отключенным JIT-компилятором:
$ java -Djava.compiler=NONE Fibonacci.java
Наконец, мы реализуем и запустим тот же алгоритм на C++ и JavaScript для сравнения.
6.2. Результаты теста производительности
Давайте посмотрим на измеренные средние показатели в наносекундах после запуска рекурсивного теста Фибоначчи:
- Java с использованием JIT-компилятора — 2726 нс — самый быстрый
- Java без JIT-компилятора — 17965 нс — на 559% медленнее
- C++ без оптимизации O2 — 9435 нс — на 246% медленнее
- C++ с оптимизацией O2 — 3639 нс — на 33% медленнее
- JavaScript — 22998 нс — на 743% медленнее
В этом примере производительность Java более чем на 500% выше при использовании JIT-компилятора . Однако для запуска JIT-компилятора требуется несколько запусков.
Интересно, что Java работал на 33% лучше, чем код C++, даже когда C++ скомпилирован с включенным флагом оптимизации O2. Как и ожидалось, C++ работал намного лучше в первых нескольких запусках , когда Java все еще интерпретировалась.
Java также превосходит аналогичный код JavaScript, выполняемый с помощью Node, который также использует JIT-компилятор. Результаты показывают повышение производительности более чем на 700%. Основная причина в том, что JIT-компилятор Java запускается гораздо быстрее .
7. Что следует учитывать
Технически можно напрямую скомпилировать код любого статического языка программирования в машинный код. Также можно интерпретировать любой программный код шаг за шагом.
Подобно многим другим современным языкам программирования, Java использует комбинацию компилятора и интерпретатора. Цель состоит в том, чтобы использовать лучшее из обоих миров, обеспечивая высокую производительность и независимое от платформы выполнение .
В этой статье мы сосредоточились на объяснении того, как все работает в HotSpot. HotSpot — это стандартная реализация JVM с открытым исходным кодом от Oracle. Graal VM также основана на HotSpot, поэтому применяются те же принципы.
В настоящее время наиболее популярные реализации JVM используют комбинацию интерпретатора и JIT-компилятора. Однако возможно, что некоторые из них используют другой подход.
8. Заключение
В этой статье мы рассмотрели Java и внутренности JVM. Нашей целью было определить, является ли Java компилируемым или интерпретируемым языком. Мы изучили компилятор Java и внутреннее устройство исполнительного механизма JVM.
На основании этого мы пришли к выводу, что Java использует комбинацию обоих подходов.
Исходный код, который мы пишем на Java, сначала компилируется в байт-код в процессе сборки. Затем JVM интерпретирует сгенерированный байт-код для выполнения. Однако JVM также использует компилятор JIT во время выполнения для повышения производительности.
Как всегда, исходный код доступен на GitHub .