Архитектура виртуальной машины Java: объяснение для начинающих
Независимо от того, приходилось ли вам разрабатывать на Java, вы, вероятно, слышали о виртуальной машине Java (JVM).
JVM — ядро экосистемы Java. Она позволяет программам на базе Java следовать принципу “написал один раз, запустил где угодно”. Вы можете написать Java-код на одной машине и запустить его на любой другой благодаря JVM.
JVM изначально разрабатывалась исключительно для поддержки Java. Однако со временем на платформе Java обосновались многие другие языки, такие как Scala, Kotlin и Groovy. Все они в совокупности называются языками JVM.
В этой статье мы расскажем больше о JVM: как она работает и из каких компонентов состоит.
Что такое виртуальная машина?
Прежде чем переходить к JVM, остановимся на самой концепции виртуальной машины (ВМ).
Виртуальная машина — это виртуальное представление физического компьютера. Виртуальную машину можно назвать гостевой, а физический компьютер, на котором она работает, — хост-машиной.
На одной физической машине может работать несколько виртуальных, каждая со своей собственной операционной системой и приложениями. Эти виртуальные машины изолированы друг от друга.
Что такое виртуальная машина Java?
В языках программирования, таких как C и C++, код сначала компилируется в машинный для конкретной платформы. Эти языки называются компилируемыми языками.
С другой стороны, в таких языках, как JavaScript и Python, компьютер выполняет инструкции напрямую, без необходимости компиляции. Эти языки называются интерпретируемыми.
Java использует комбинацию обоих методов. Код Java сначала компилируется в байтовый код и генерирует файл класса ( .class ). Этот файл класса затем интерпретируется виртуальной машиной Java для базовой платформы. Один и тот же файл класса может выполняться на любой версии JVM, на любой платформе и операционной системе.
Подобно обычным виртуальным машинам, JVM создает изолированное пространство на хост-машине. Это пространство может использоваться для выполнения Java-программ независимо от платформы или операционной системы компьютера.
Архитектура виртуальной машины Java
JVM состоит из трех отдельных компонентов:
- загрузчик классов;
- область памяти/данных среды выполнения;
- механизм выполнения.
Рассмотрим каждый из них более подробно.
Загрузчик классов
Когда вы компилируете исходный файл .java , он преобразуется в байт-код как файл .class . Когда вы вызываете этот класс у себя в программе, загрузчик классов загружает его в основную память.
Как правило, первым в память загружается класс, содержащий метод main() .
Процесс загрузки класса состоит из трех этапов: загрузка, связывание и инициализация.
Загрузка
Загрузка включает бинарную репрезентацию (байт-код) класса или интерфейса с определенным именем и создание на его основе исходного класса или интерфейса.
В Java доступны три встроенных загрузчика классов:
- Загрузчик классов начальной загрузки(Bootstrap Class Loader) — корневой загрузчик классов. Это суперкласс загрузчика классов расширений, который загружает стандартные пакеты Java, такие как java.lang , java.net , java.util , java.io и так далее. Эти пакеты находятся внутри rt.jar и других основных библиотек, присутствующих в каталоге $JAVA_HOME/jre/lib .
- Загрузчик классов расширений (Extension Class Loader) — подкласс загрузчика классов начальной загрузки и суперкласс загрузчика классов приложений. Он загружает расширения стандартных библиотек Java, которые присутствуют в каталоге $JAVA_HOME/jre/lib/ext .
- Загрузчик классов приложений (Application Class Loader) — конечный загрузчик классов и подкласс загрузчика классов расширений. Он загружает файлы, которые находятся в пути к классам (classpath). По умолчанию путь к классу устанавливается как текущий каталог приложения. Путь к классу также можно изменить, добавив параметр командной строки -classpath или -cp .
JVM использует метод ClassLoader.loadClass() для загрузки класса в память. Он пытается загрузить класс на основе полного имени.
Если родительский загрузчик классов не может найти класс, он делегирует работу дочернему загрузчику классов. Если последний загрузчик также не может загрузить класс, он создает исключение NoClassDefFoundError или ClassNotFoundException .
Связывание
После загрузки класса в память происходит процесс связывания. Связывание класса или интерфейса предполагает объединение различных элементов и зависимостей программы.
Связывание включает следующие шаги.
- Проверка. На этом этапе проверяется структурная корректность файла .class путем проверки его на соответствие набору ограничений и правил. Если проверка по какой-либо причине завершается неудачей, выбрасывается исключение VerifyException .
Например, если код был создан на Java 11, но выполняется в системе, где установлена Java 8, этап проверки завершится неудачно.
- Подготовка. На этом этапе JVM выделяет память для статических полей класса или интерфейса и инициализирует их значениями по умолчанию.
Предположим, что вы объявили в классе следующую переменную:
private static final boolean enabled = true;
На этапе подготовки JVM выделяет память для переменной enabled и устанавливает ее значение в значение по умолчанию для логического значения, которое равно false .
- Решение. На этом этапе символические ссылки заменяются прямыми, присутствующими в пуле констант времени выполнения.
Например, если у вас есть ссылки на другие классы или постоянные переменные, присутствующие в других классах, они разрешаются на этом этапе и заменяются их фактическими ссылками.
Инициализация
Инициализация включает выполнение метода инициализации класса или интерфейса (известного как ). Сюда может входить вызов конструктора класса, выполнение статического блока и присвоение значений всем статическим переменным. Это заключительный этап загрузки класса.
К примеру, ранее мы объявили следующее:
private static final boolean enabled = true;
На этапе подготовки переменной enabled было присвоено значение по умолчанию false . На этапе инициализации этой переменной присваивается ее фактическое значение true .
Примечание: JVM имеет многопоточный характер. Может случиться так, что несколько потоков одновременно пытаются инициализировать один и тот же класс. Это может привести к проблемам параллелизма. Чтобы гарантировать правильную работу программы в многопоточной среде, необходимо обеспечить потокобезопасность.
Область данных среды выполнения
В области данных среды выполнения есть пять компонентов:
Рассмотрим каждый из них в отдельности.
Область метода
Здесь хранятся все данные уровня класса, такие как пул констант времени выполнения, данные полей и методов, а также код методов и конструкторов.
Если памяти, доступной в области метода, недостаточно для запуска программы, JVM выдает ошибку OutOfMemoryError .
Например, предположим, что мы объявили следующий класс:
public class Employee
private String name;
private int age;
public Employee(String name, int age)
this.name = name;
this.age = age;
>
>
В этом примере данные уровня поля, такие как name и age , а также сведения о конструкторе загружаются в область метода.
Область метода создается при запуске виртуальной машины, и на каждую виртуальную машину приходится только одна область метода.
Область кучи
Здесь хранятся все объекты и соответствующие им переменные экземпляра. Это область данных времени выполнения, из которой выделяется память для всех экземпляров классов и массивов.
Например, предположим, вы объявили следующий экземпляр:
Employee employee = new Employee();
В этом примере создается экземпляр класса Employee и загружается в область кучи.
Куча создается при запуске виртуальной машины, и на каждую виртуальную машину приходится только одна область кучи.
Примечание: поскольку области метода и кучи совместно используют одну и ту же память для нескольких потоков, данные, хранящиеся здесь, не потокобезопасны.
Область стека
Всякий раз, когда в JVM создается новый поток, одновременно создается отдельный стек среды выполнения. Все локальные переменные, вызовы методов и частичные результаты хранятся в области стека.
Если для обработки в потоке требуется больший размер стека, чем доступно, JVM выдает ошибку StackOverflowError .
Для каждого вызова метода в памяти стека делается одна запись, которая называется фреймом стека. Когда вызов метода завершен, фрейм стека уничтожается.
Фрейм стека разделен на три части.
- Локальные переменные. Каждый фрейм содержит массив переменных, известных как его локальные переменные. Здесь хранятся все локальные переменные и их значения. Длина этого массива определяется во время компиляции.
- Стек операндов. Каждый фрейм содержит стек последним-вошел-первым-вышел (last-in-first-out, LIFO), известный как стек операндов. Он действует как рабочая область среды выполнения для любых промежуточных операций. Максимальная глубина этого стека определяется во время компиляции.
- Данные фрейма. Здесь хранятся все символы, соответствующие методу. Здесь также хранится информация о блоке catch на случай исключений.
К примеру, есть следующий код:
double calculateNormalisedScore(List answers)
double score = getScore(answers);
return normalizeScore(score);
>
double normalizeScore(double score)
return (score – minScore) / (maxScore – minScore);
>
В этом примере кода переменные, такие как answers и score , помещаются в массив локальных переменных. Стек операндов содержит переменные и операторы, необходимые для выполнения математических операций вычитания и деления.
Примечание: поскольку область стека не является общей, она по своей сути потокобезопасна.
Регистры счетчика программ
JVM поддерживает многопоточность. Каждый поток имеет собственный регистр счетчика программ для хранения адреса выполняемой в данный момент инструкции JVM. Как только инструкция выполнена, регистр обновляется следующей инструкцией.
Стеки нативных методов
JVM содержит стеки, которые поддерживают нативные методы, то есть такие методы, которые написаны на языке, отличном от Java, например C или C++. Для каждого нового потока также выделяется отдельный стек нативных методов.
Система выполнения
Как только байт-код загружен в основную память, и подробная информация становится доступна в области данных среды выполнения, наступает следующий этап — запуск программы. Механизм выполнения делает это, выполняя код из каждого класса.
Однако перед выполнением программы байт-код необходимо преобразовать в инструкции машинного языка. В качестве механизма выполнения JVM может задействовать интерпретатор или JIT-компилятор.
Интерпретатор
Интерпретатор считывает и выполняет инструкции байт-кода строка за строкой. Из-за построчного выполнения интерпретатор работает сравнительно медленнее.
Еще один недостаток интерпретатора — при многократном вызове метода каждый раз требуется новая интерпретация.
JIT-компилятор
JIT-компилятор преодолевает недостаток интерпретатора. Механизм выполнения сначала использует интерпретатор для выполнения байт-кода, но когда он находит какой-то повторяющийся код, то задействует JIT-компилятор.
Затем JIT-компилятор компилирует весь байт-код и изменяет его на собственный машинный код. Этот собственный машинный код используется непосредственно для повторных вызовов методов, что повышает производительность системы.
JIT-компилятор содержит следующие компоненты.
- Генератор промежуточного кода — генерирует промежуточный код.
- Оптимизатор кода — оптимизирует промежуточный код для повышения производительности.
- Генератор целевого кода — преобразует промежуточный код в собственный машинный код.
- Профилировщик — находит горячие точки (код, который выполняется повторно).
Чтобы лучше понять разницу между интерпретатором и JIT-компилятором, предположим, что у вас есть следующий код:
int sum = 10;
for(int i = 0 ; i sum += i;
>
System.out.println(sum);
Интерпретатор будет извлекать из памяти значение sum для каждой итерации в цикле, добавлять к нему значение i и записывать обратно в память. Это дорогостоящая операция, потому что каждый раз при входе в цикл происходит обращение к памяти.
Однако JIT-компилятор распознает, что в этом коде есть “горячая точка”, и выполнит оптимизацию. Он сохранит локальную копию sum в регистре для потока и будет продолжать добавлять значение i в цикле. Как только цикл завершится, компилятор запишет значение sum обратно в память.
Примечание: компилятору требуется больше времени для компиляции кода, чем интерпретатору для интерпретации кода строка за строкой. Если вы намерены запустить программу только один раз, интерпретатор будет предпочтительнее.
Сборщик мусора
Сборщик мусора (Garbage Collector, GC) собирает и удаляет объекты без ссылок из области кучи. Это процесс автоматического восстановления неиспользуемой памяти во время выполнения путем уничтожения мусорных объектов.
Сборка мусора делает память Java эффективной, потому что удаляет объекты без ссылок из памяти кучи и освобождает место для новых объектов. Она включает два этапа:
- Пометка — на этом этапе GC идентифицирует неиспользуемые объекты в памяти.
- Очистка — на этом этапе GC удаляет объекты, идентифицированные на предыдущем этапе.
Сборка мусора выполняется JVM автоматически через регулярные промежутки времени и не требует отдельной обработки. Ее также можно запустить вызовом System.gc() , но выполнение не гарантируется.
JVM содержит три различных типа сборщиков мусора.
- Последовательная сборка мусора. Это самая простая реализация GC. Она предназначена для небольших приложений, работающих в однопоточных средах. Для сборки мусора используется один поток. Запуск приводит к событию “остановки мира”, когда все приложение приостанавливает работу. Аргумент JVM для запуск последовательного сборщика мусора: -XX:+UseSerialGC .
- Параллельная сборка мусора. Это реализация GC по умолчанию, также известная как сборщик пропускной способности. Для сборки мусора в нем используется несколько потоков, но работа приложения все равно приостанавливается при запуске. Аргумент JVM для параллельного сборщика мусора: -XX:+UseParallelGC .
- Garbage First (G1). G1 был разработан для многопоточных приложений с большим доступным размером кучи (более 4 ГБ). Он разбивает кучу на набор областей одинакового размера и использует несколько потоков для их сканирования. G1-сборщик определяет регионы с наибольшим количеством мусора и сначала выполняет сбор мусора в них. Аргумент JVM для этого сборщика мусора: -XX:+UseG1GC .
Примечание: существует другой тип сборщика мусора, называемый сборщиком параллельных меток (CMS). Однако он устарел начиная с Java 9 и полностью удален в Java 14, и его место занимает сборщик G1.
Нативный интерфейс Java (Java Native Interface, JNI)
Иногда необходимо задействовать в работе нативный (не Java) код (например, написанный на C/C++). К примеру, в тех случаях, когда нужно взаимодействовать с физическим оборудованием или преодолевать ограничения по управлению памятью и производительности в Java. Java поддерживает выполнение нативного кода через нативный интерфейс Java (JNI).
JNI действует как мост для предоставления вспомогательных пакетов другим языкам программирования, таким как C, C++ и так далее. Это особенно полезно в тех случаях, когда нужно написать код, который не полностью поддерживается Java, например, некоторые специфичные для платформы функции могут быть написаны только на C.
Вы можете воспользоваться ключевым словом native , чтобы указать, что реализация метода будет предоставлена нативной библиотекой. Также потребуется вызвать System.LoadLibrary() , чтобы загрузить общую нативную библиотеку в память и сделать ее функции доступными для Java.
Нативные библиотеки методов
Нативные библиотеки методов — это библиотеки, написанные на других языках программирования, таких как C, C++ и ассемблер. Эти библиотеки обычно представлены в виде файлов .dll или .so . Такие библиотеки можно загружать через JNI.
Распространенные ошибки JVM
- ClassNotFoundException . Происходит, когда загрузчик классов пытается загрузить классы с помощью Class.forName() , ClassLoader.loadClass() или ClassLoader.findsystemclass() , но определение класса с указанным именем не найдено.
- NoClassDefFoundError . Происходит, когда компилятор успешно скомпилировал класс, но загрузчик классов не может найти файл класса во время выполнения.
- OutOfMemoryError . Происходит, когда JVM не может выделить объект из-за нехватки памяти, и сборщик мусора не может предоставить больше памяти.
- StackOverflowError . Происходит, если в JVM не хватает места при создании новых кадров стека во время обработки потока.
Заключение
В этой статье мы обсудили архитектуру виртуальной машины Java и ее компоненты. Часто мы не вникаем глубоко во внутреннюю механику JVM или не интересуемся, как она работает, пока работает код.
Только когда что-то идет не так, и нам нужно настроить JVM или устранить утечку памяти, мы пытаемся понять ее внутреннее устройство.
Это также очень популярный вопрос во время интервью на роль бэкенд-разработчика — как младшего, так и старшего уровня. Глубокое понимание JVM поможет вам лучше писать код и избегать подводных камней, связанных со стеком и ошибками памяти.
- 9 вещей, которыми следует заняться Java программисту в 2018 году
- Превратите свой Java-код в полностью асинхронный
- Циклы Java в сторону — даешь потоки!
JVM – что это? Как устроена виртуальная машина Java
Виртуальная машина Java («Java Virtual Machine» — JVM) — это основная часть платформы Java Runtime Environment (JRE), которая интерпретирует байт-код Java для запуска программ.
Одним из наиболее значительных преимуществ использования является использование JVM для запуска программы Java в любой операционной среде. В её основе реализуется принцип WORA (Write once, run anywhere — «написал один раз, запускай везде»), который сильно упростил процессы разработки.
Как правило, считается, что JVM имеет двойное определение — техническое и неформальное.
Техническое: JVM — это спецификация программы, которая обеспечивает среду выполнения кода Java.
Неофициальное: JVM запускает приложения Java, c помощью настроенных параметров для управления всеми программными ресурсами.
Что делает виртуальная машина Java
JVM выполняет две основные функции:
- позволяет запускать Java-программы на любом устройстве или в любой операционной системе;
- даёт доступ к управлению памятью программ и её оптимизации.
Поскольку JVM — это хорошо известная среда выполнения со стандартизированной конфигурацией, мониторингом и управлением, она естественным образом подходит для контейнерной разработки с использованием таких технологий, как Docker.
Роль Java Virtual Machine
Теперь давайте более подробно рассмотрим место виртуальной машины Java в выполняемых процессах.
.jpg)
Если вы внимательно посмотрите на схему, добавленную выше, то будет несложно догадаться, что JVM образует слой между операционной системой и программами Java.
Это означает, что скомпилированная Java-программа будет связываться с Java Virtual Machine, а JVM будет общаться с операционной системой, являясь своего рода посредником между скомпилированными файлами классов и операционной системой.
Файл .class и байт-код
Когда дело доходит до выполнения программы, главное, что интересует виртуальную машину Java — это определённый формат файла – .class.
Файлы классов содержат наполовину скомпилированный код, называемый байт-кодом, который в свою очередь предоставляет JVM инструкции, таблицу символов и другую вспомогательную информацию.
Архитектура виртуальной машины Java
Понять, что такое виртуальная машина Java, будет немного проще, если познакомиться с её архитектурой и тем, как она работает. Поэтому важно рассмотреть строение JVM и особенности её частей.
Java Virtual Machine состоит из трёх отдельных компонентов:
- загрузчик классов;
- область памяти;
- исполнительный механизм.
(4).jpg)
Загрузчики классов отвечают за динамическую загрузку файлов .class в виртуальную машину Java и сохранения байт-кода в области метода JVM, о которой мы поговорим чуть позже.
Загрузчик классов Java бывает трёх типов:
- BootStrap ClassLoader (Загрузчик классов начальной загрузки) — это машинный код, который запускает операцию, когда её вызывает JVM. Его задача — загрузить классы из папки rt.jar.
- Extension ClassLoader (Загрузчик классов расширений) является дочерним элементом Bootstrap ClassLoader и загружает расширения основных классов Java из каталога jre/lib/ext или любого другого каталога, на который указывает java.ext.dirs.
- System ClassLoader (Системный загрузчик классов) загружает классы, найденные в переменной среды CLASSPATH, -classpath или параметре командной строки -cp.
- Область памяти
Область памяти или, как её ещё называют, область данных времени выполнения JVM состоит из 5 частей:
- Область метода предназначена для хранения данных файлов .class: например, метаданные, данные полей и методов, а также код метода. Область метода создаётся автоматически при запуске виртуальной машины, и для каждой ВМ существует только одна область метода.
- Область кучи. В куче хранятся все объекты и соответствующие им переменные экземпляра. Когда мы создаём новый экземпляр класса, он сразу же загружается в область кучи, которая остается единственной во время выполнения задачи.
- Область стека. В неё загружаются все локальные переменные, вызовы методов и частичные результаты.
- Регистры ПК. В регистре ПК хранится адреса виртуальных машин Java, выполняющих операцию в данный момент. В Java каждый поток получает свой собственный регистр ПК.
- Стеки нативных методов. Нативные методы — это методы, написанные на C или C++. Виртуальная машина JVM хранит стеки, которые поддерживают такие методы, с отдельным стеком собственных методов, выделенным для каждого потока.
- Исполнительный механизм
Этот тип программного обеспечения используется для тестирования оборудования и программного обеспечения. При этом он не сохраняет никакой информации о тестируемом продукте.
- интерпретатора;
- JIT-компилятора;
- сборщика мусора.
Перед выполнением программы интерпретатор и компилятор JIT («Just-in-time» — «точно в нужное время») преобразуют байт-код в машинные инструкции. Интерпретатор делает это построчно.
В тот момент, когда в сценарии обнаруживается повторяющийся код, к нему подключается JIT-компилятор для ускорения операции. Затем он компилирует байт-код и заменяет его собственным кодом. Такой процесс повышает производительность всей системы.
Но за что же в таком случае отвечает сборщик мусора? В некоторых других языках программирования (таких как C++) освобождение памяти от объектов без циклических ссылок зависит лишь от самого программиста. Однако в JVM этим занимается сборщик мусора, что оптимизирует использование памяти.
Важно отметить, что сборка мусора выполняется в JVM автоматически через определённые отрезки времени и не требует отдельного внимания специалистов. Конечно, этот процесс можно попробовать принудительно запустить, вызвав System.gc(), но нет никакой гарантии, что это сработает.
Заключение
Несмотря на то, что виртуальная машина Java изначально предназначалась только для запуска и выполнения программ Java, на сегодняшний день она способна поддерживать многие языки сценариев и программирования, что лишь укрепило популярность данной платформы.
В этой статье мы познакомились с виртуальной машиной Java, её архитектурой и основными компонентами, а также поговорили о месте JVM в запрашиваемых системой операциях.
Виртуальная машина Java
В этой статье мы расскажем, что такое виртуальная машина Java и как ее установить.
Что такое виртуальная машина Java и для чего она нужна
Виртуальная машина Java (JVM) — это ключевой компонент платформы Java. С ее помощью можно запускать программы Java и продукты, написанные с применением байт-кода. А также она помогает управлять ресурсами программ во время их выполнения.
Перед тем как мы будем подробнее рассматривать виртуальную машину, разберем, что такое байт-код.
Сам по себе процессор — это сложный калькулятор. У него есть множество ячеек памяти, между которыми проводятся различные математические и байтовые операции. Как проводить каждую операцию, какой будет последовательность действий и какие данные нужны для операции ― всё это записано в машинном коде. Машинный код — это единственный язык, который понимает процессор любого компьютера.
Если бы все процессоры работали с помощью одинаковых машинных кодов, при разработке программ не нужно было подстраиваться под каждую систему. Достаточно было бы выучить один «машинный язык» и с его помощью удалось бы общаться со всеми процессорами. Однако в реальности процессоры «говорят» на разных языках. Языки могут отличаться:
- архитектурой CISC и RISC,
- длиной команд,
- режимом адресации,
- сложностью кодировок инструкций.
Из-за этих различий программы, созданные для одной архитектуры (или одного поколения процессоров), не работают на другой. Поэтому разработчикам приходится перекомпилировать программы для работы на других компьютерах.
Чтобы решить эту проблему, был создан байт-код — своеобразный промежуточный код между машинными языками. Прочитать байт-код может виртуальная машина Java.
Когда вы запускаете приложение Java на компьютере, байт-код программы попадает на JVM. Интерпретатор в виртуальной машине компилирует байт-код программы в понятный для процессора машинный код. Таким образом, одна программа, созданная на Java, может открываться на разных процессорах.
Как установить виртуальную машину Java
Существует два продукта, с помощью которых можно создать ВМ:
- Java Runtime Environment (JRE) ― виртуальная машина и API. Подходит, если вы хотите использовать готовые программы на Java.
- Java Development Kit (JDK) ― виртуальная машина, API и средства разработки программ на Java. Нужна тем, кто планирует писать свои Java-программы.
Как установить JRE
Чаще всего JRE уже есть на многих устройствах. Поэтому перед тем как устанавливать JRE проверьте, есть ли она на устройстве. Для этого зайдите в терминал и введите команду:
java -version
Команда для всех операционных систем одинаковая. Если на вашем устройстве установлена Java, перед вами появится сообщение о версии программы:

Если нет, то в сообщение будет ошибка или система скажет, что не знает такой команды. В таком случае вам понадобится установить JRE самостоятельно.
Установка ВМ на разных операционных системах похожа. В качестве примера покажем установку на macOS.

В зависимости от вашей операционной системы загрузите установочный файл с официального сайта. Например, если вам нужна виртуальная машина Java для Windows 10 x64, выберите пункт, показанный на картинке ниже:

Откройте папку со скачанным файлом и дважды кликните по нему:

Так как файл скачан из интернета, система попросит у вас разрешение на установку:

Затем система попросит ввести пароль пользователя. Введите его и нажмите OK:

Нажмите Install:
Затем пройдите по подсказкам на экране.
Готово, вы установили Java.
Как установить JDK
Скачайте установочный файл с официальной страницы.

Запустите установку:

Нажмите Продолжить:

Нажмите Установить:

Готово, программа установлена. Нажмите Закрыть:
Помогла ли вам статья?
Спасибо за оценку. Рады помочь
Как устроена Java Virtual Machine и её экосистема: подробный гайд
Иван Углянский рассказал, как устроена JVM, что с ней не так и зачем Python, Ruby и другие языки переезжают на виртуальную машину Java.


Иллюстрация: Mark Kingston / WikiMedia Commons / Colowgee для Skillbox Media

Антон Сёмин
Пишет об истории IT, разработке и советской кибернетике. Знает Python, JavaScript и немного C++, но предпочитает писать на русском.

об эксперте
JVM engineer. Работает в Excelsior@Huawei над виртуальными машинами Java, компиляторами и новыми языками программирования. Один из основателей и лидеров JUGNsk, член программного комитета SnowOne Conference.
ссылки
Я занимаюсь JVM более 10 лет. Начинал в новосибирской компании Excelsior, которая разрабатывала свою реализацию виртуальной машины Java — Excelsior JET. Потом мы вошли в состав Huawei, но команда осталась прежней.
Что такое JVM и как она спасла программистов от профессионального «подгорания»
До появления Java, примерно 25 лет назад, многие приложения писали на С или С++. У этих языков есть проблема: разработчику приходится думать, на какой операционке и архитектуре процессора будет работать его код. Например, если он пишет программу под Linux, то, скорее всего, она не запустится на Windows или MacOS. Поэтому код приходилось пичкать директивами условной компиляции или писать отдельную версию для каждой операционки.
Самую сильную боль вызывали приложения с GUI — код графических компонентов для разных операционок был совершенно разным. Причина в том, что C и C++ довольно близки к железу, а значит, сишник должен учитывать архитектуру процессора и тип операционной системы. Понятно, что никакой кроссплатформенностью здесь и не пахнет.
Ещё в 1960-е годы у инженеров появилась идея: писать программы не для конкретного железа, а для абстрактного «исполнителя». Программы на Java как раз пишутся для такого исполнителя — виртуальной машины, или Java Virtual Machine (JVM). Java-разработчик не задумывается, на какой платформе будет запускаться его код. В то же время виртуальная машина не знает, что исполняет инструкции на Java, ведь она принимает и исполняет байт-код.
В этом и было «уникальное торговое предложение» Java: разработчики писали программу под Windows, а она запускалась на macOS почти без изменений. Сейчас это звучит вполне естественно, а 25 лет назад казалось настоящим чудом.

Кроссплатформенность в Java обеспечивается явным разделением уровней языка и реализации.
Языковой уровень. Разработчики пишут код на языке Java, синтаксис и семантика которого описаны в Java Language Specification. После этого специальным инструментом, который называется javac, исходный код компилируется в байт-код Java. При этом происходит проверка синтаксиса, и в случае его нарушения разработчик получает сообщение об ошибке от javac.
Что здесь важно:
- На этом этапе нет ничего платформенно-специфичного, весь код на языке Java (как и байт-код Java) универсален.
- Байт-код Java — это язык, предназначенный не для людей, а для машин. Обычному разработчику его читать не нужно.
Уровень реализации. Полученный байт-код Java передаётся на вход виртуальной машины Java. И вот как именно она будет его исполнять, описано уже в другой спецификации — Java Virtual Machine Specification. Со всеми особенностями конкретной операционной системы или архитектуры процессора тоже разбирается JVM, без влияния на исходный код на языке Java. Таким образом, происходит перенос ответственности: разработчики на Java о таких неприятных вещах больше не думают, им достаточно просто взять правильную JVM (например, для Linux x64). А все OS/arch-специфические нюансы решают разработчики JVM.
Язык Java и сама JVM разрабатываются параллельно, при этом новые фичи языка зачастую требуют поддержки внутри JVM. С другой стороны, различные реализации JVM развиваются и сами по себе, например в них появляются новые, более эффективные алгоритмы сборки мусора или оптимизации кода.
У разделения языка и его реализации есть замечательное следствие: разработчику не обязательно ограничиваться языком Java, ведь виртуальная машина понятия не имеет, откуда взяли байт-код, который пришёл к ней на вход, был ли это изначально Java-код или что-то другое. То есть программу можно писать на любом языке, который транслируется в байт-код. И таких языков — целое семейство: Kotlin, Clojure, Groovy и так далее. Например, программы на Kotlin компилируются с помощью kotlinc в class-файлы, а затем подаются на вход JVM.
Как соотносятся JDK и Open JDK
Java Development Kit (JDK) — это комплект инструментов для разработки приложений на Java. В него входят несколько компонентов, которые позволяют разрабатывать и запускать приложения:
- Javас. Это компилятор, который преобразует исходники Java в class-файлы или формат jar.
- JVM. Виртуальная машина, которая исполняет байт-код.
- Стандартная библиотека. Набор модулей, классов, объектов и функций, которые доступны в языке.
- Документация. В ней содержится справочная информация об инструментах данной версии JDK.
Когда появляется новая версия языка, все эти компоненты обновляются.
В случае стандартной JVM (например, HotSpot) запуск Java-приложения выглядит так: сначала с помощью javac вы компилируете исходный код в class-файлы (или jar-файлы), и именно их вы можете поставлять в качестве приложения.
Чтобы приложение запустилось на хосте, там должна быть установлена виртуальная машина. Тогда пользователь пишет в командной строке: java -jar имя вашего jar-файла> — и программа запускается.
С нашей виртуальной машиной сценарий запуска приложения был другой: мы заранее компилировали class-файлы в один исполняемый exe-файл. Поэтому он работал на компьютере пользователя без каких-либо предустановленных JVM. Но, по сути, JVM никуда не исчезла — просто все её части были слинкованы в один экзешник.
В спецификации Java Virtual Machine почти ничего не сказано о том, как именно её реализовывать — только что должно получиться на выходе. Например, вы не можете явно освободить память из под объекта, когда он вам уже не нужен. Этим занимается специальный компонент JVM — Garbage Collector. Но вот как именно он работает, в спецификации не сказано.
Так и было задумано: спецификация позволяет создавать разные JVM, реализации которых в чём-то отличаются, но при этом удовлетворяют требованиям спецификации. И это замечательно, пусть расцветают сто цветов! Такие различия в реализации дают разным виртуальным машинам особые конкурентные преимущества: у кого-то более крутой сборщик мусора, у кого-то быстрый стартап, а кто-то реализовал более мощные оптимизации в компиляторе.
Первую JVM, как и язык Java, разработали в компании Sun Microsystems. Когда Sun опубликовала спецификацию, Oracle стала работать над своей JRockit, а IBM — над OpenJ9. Наша Excelsior JET вышла в свет в 2000 году.
Изначально все JVM были закрытыми, но в 2006 году Sun опубликовала исходники своей JVM HotSpot и компилятора javac, а потом и стандартной библиотеки Java. Это дало старт проекту OpenJDK — полностью открытой реализации JDK. Через какое-то время Sun была поглощена корпорацией Oracle, но при этом проект OpenJDK никуда не делся и до сих пор остаётся главной open-source-площадкой для разработки Java и JVM.
OpenJDK — это эталонная реализация, на которую ориентируются разработчики других виртуальных машин. Проект распространяется под GPL 2 + Classpath Exception, благодаря чему любая компания или разработчик, которым хватит квалификации, смогут форкнуть OpenJDK и начать разрабатывать свою собственную JVM. Конечно же, по условию лицензии их реализация тоже будет open source.
Существуют и реализации JVM, которые никак не связаны с OpenJDK. Ведь не только Sun Microsystems и Oracle создавали JVM. Другой пример — это IBM J9, которая, кстати, тоже открыла исходный код и стала называться OpenJ9.
Некоторые компании продают свои виртуальные машины. Например, у Azul есть свободная сборка Zulu и закрытая коммерческая Azul Zing. В последней есть уникальный сборщик мусора C4, который гарантировал низкие паузы ещё до того, как это стало мейнстримом. А также технология ReadyNow!, которая позволяет «запомнить» список всех оптимизаций кода и «проиграть» его на старте JVM, выводя машину на пиковую производительность.
Excelsior JET тоже была закрытой и платной. Она обеспечивала быстрый стартап, низкое потребление памяти и хорошую пиковую производительность во многих сценариях. Всё это мы получали за счёт статической компиляции, в отличие от динамической компиляции на лету в HotSpot.
Однако сегодня люди чаще пользуются бесплатными сборками, основанными на OpenJDK. Существуют как стандартные сборки OpenJDK от Oracle, так и сборки от других компаний и разработчиков, имеющих отношение к разработке OpenJDK (зачастую их публикуют контрибьюторы в OpenJDK не из Oracle).
В таких сборках могут быть дополнительные фичи, не включённые по умолчанию в стандартные сборки от Oracle. Либо такие альтернативные сборки прошли дополнительную сертификацию, например для использования с «КриптоПро». При этом такие сторонние билды проходят все стандартные тесты, поэтому гарантируется, что ваши программы будут работать как надо и пользоваться ими можно абсолютно безопасно и бесплатно.
Некоторые компании не только выкладывают свои билды, но и оказывают платную поддержку пользователям. Допустим, возникла проблема с виртуальной машиной: ваше приложение работает неожиданно медленно или даже крашится! Всякое бывает, ведь JVM — это тоже программа, в которой могут быть свои баги. Тогда инженеры этой компании оперативно разберутся в проблеме и решат её.
В целом вариантов виртуальных машин и их сборок очень много — в рамках этой статьи все не перечислить. Есть сборки от Oracle, Red Hat, Azul, IBM, BellSoft, Amazon и Microsoft. Сообщество Java-чемпионов написало замечательный документ, в котором перечислило все варианты и в целом разъяснило ситуацию с многообразием JVM и их сборок. Документ называется Java Is Still Free, рекомендую его всем к прочтению.
Как и зачем другие языки переходят на JVM
Java — это не просто язык, а целая философия. И она оказалась настолько востребованной, что у других языков программирования стали появляться реализации под JVM. Например, Jython и JRuby.
Главная причина появления таких реализаций — перформанс. За 25 лет человечество научилось делать хорошо оптимизированные JVM. Так почему бы за счёт этого не повысить производительность программ на Python и Ruby?
С другой стороны, так можно получить хорошую связь с экосистемой Java. Если мы запускаем код на JVM, он лучше взаимодействует с другими модулями на JVM. Значит, можно пользоваться и Java-библиотеками.
У Kotlin немного другая история. Когда язык только появился, было решено компилировать его в байт-код Java и запускать на JVM. Таким образом, реализация языка заключалась в написании хорошего транслятора — kotlinc — из исходного кода .kt в class-файлы. А реализация низкоуровневых компонентов типа сборщиков мусора или взаимодействия с операционной системой делегировалась уже существующим JVM.
Относительно недавно создатели Kotlin сделали Kotlin Native — технологию компиляции Kotlin в нативный код напрямую, без JVM. В нём низкоуровневые компоненты, такие как менеджер памяти, уже реализованы самостоятельно.
Хотя на словах кажется, что любой язык программирования легко перевести на JVM (достаточно написать транслятор в class-файлы), на деле всё оказывается намного сложнее. Грамотно отобразить фичи языка в байт-код Java и при этом получить хорошую производительность — нетривиальная задача. Посмотрите хотя бы на JRuby.
Чарльз Наттер часто делится опытом поддержки различных фич Ruby в проекте JRuby и рассказывает о сложностях, с которыми сталкивается. Чем дальше исходный язык от Java, тем сложнее задача трансляции. Но, думаю, всё возможно, и JRuby — наглядный тому пример.
Какие конкуренты есть у JVM
Самый известный конкурент Java Virtual Machine — платформа .NET и их виртуальная машина для реализации C#.
В начале 2000-х Microsoft делала свою виртуальную машину Java — Microsoft J++. Но из-за того, что корпорация не соблюдала спецификацию, Sun подала на неё судебный иск. Microsoft проиграла все суды и лишилась права делать виртуальную машину для Java. Есть мнение, что это стало одной из причин появления C#.
Java и C# решают одни и те же задачи и обладают одними и теми же преимуществами: строгая типизация, сборка мусора, безопасность — всё это совпадает. Это managed-языки широкого применения.
Раньше между платформами была одна принципиальная разница: Java был кроссплатформенным, а .NET и C# работали только на Windows. Но с тех пор, как появился .NET Core, C# тоже стал мультиплатформенным.
Отмечу, что сами языки Java и C# довольно разные, и C# развивается быстрее: там регулярно появляются интересные фичи, которые в Java приходят с большим опозданием или не приходят вообще. Поэтому как язык C# в целом выглядит интереснее.
Зато у Java мощная реализация и хорошо оптимизированные виртуальные машины. Это стало возможным благодаря «гонке вооружений» между разработчиками виртуальных машин. Вендоры на протяжении 20 лет соревновались, кто сделает самую крутую оптимизацию, лучший алгоритм сборки мусора, более быстрый стартап и так далее. Поэтому по многим показателям реализация Java выглядит куда лучше C#. Некоторые вещи, которые уже давно есть в JVM, только-только появляются в .NET.
Есть и другие языки, которые претендуют на роль конкурента Java, но они отличаются гораздо сильнее. Например, Python, который недавно занял 1-е место в рейтинге TIOBE. Он популярен в Data Science, AI, машинном обучении, скриптах и так далее. К сожалению, динамическая типизация не позволяет держать на нём слишком большую кодовую базу.
Ещё есть безумно популярный JavaScript, но его интересы с Java не очень пересекаются, так как сегодня Java во фронтенде не используется. Хотя когда-то были и такие планы (привет устаревшей технологии Java-апплетов).
Где Java хорош, а где — нет
Java широко используют в бэкенде, то есть во всех приложениях, которые работают на стороне сервера. Его главный конкурент — C#, но Java всё-таки значительно лидирует, потому что считается более кроссплатформенным. Хотя последнее уже не совсем правда. Также стоит отметить, что у Java банально есть фора по времени по сравнению с C#. Он появился на 10 лет раньше, поэтому успел захватить умы разработчиков и кодовые базы.
Почему Java, а не «швейцарский нож» С++? Просто Java удобнее и безопаснее для жизни, или, как принято говорить, не даст выстрелить себе в ногу. Разработчику не нужно думать об управлении памятью и сегфолтах, которые могу прилететь хоть откуда. Поэтому, когда дело касается будничных задач бэкенда, скорость разработки на Java и C# гораздо выше, чем на C++.
Java плохо подходит для приложений, которым требуется высокая производительность и гарантированное время отклика. Тем не менее некоторые пишут на Java и такие программы, если готовы мириться с небольшими задержками в микросекунды. Для них выигрыш от скорости разработки и надёжности важнее. Но писать на Java программное обеспечение для кардиостимуляторов, где все элементы должны работать точно, как швейцарские часы, — не стоит.

С другой стороны, приложения для трейдинга всё чаще пишут именно на Java. Казалось бы, где, если не в трейдинге, важен быстрый отклик — чтобы не потерять миллионы из-за скачка цен на акции Tesla? Однако на практике надёжность здесь важнее скорости: лучше торговать медленнее, но спокойно и надёжно, чем написать на С++ быструю биржу, которая через пять минут торгов крашнется. Java гарантирует безопасность, и это круто.
Что не так с JVM
В истории развития любого языка программирования всегда находятся тонкие места или сомнительные решения. Java здесь не исключение, встречаются такие спорные вопросы и в спецификациях Java и JVM.
Система модулей
Одна из спорных тем — Project Jigsaw и система модулей, появившаяся в девятой версии Java. Создатели Java долго делали систему, которая позволяет разбивать Java-программу на отдельные логические составляющие — модули. Это дополнительный уровень абстракции над давно существующими packages в Java.
На неё потратили много времени — ведь нужно было организовать поддержку в самом языке, виртуальной машине и других частях JDK. Система долго не попадала в релизную версию, а когда всё-таки попала, оказалась, мягко говоря, не слишком востребованной пользователями.
Как в языке появляются новые фичи и спецификации? Есть специальный совет Java Community Process (JCP), который рассматривает предложения Java Specification Request (JSR) и голосует за или против. В этом совете есть представители крупных компаний, есть представители user group и даже отдельные люди — в общем, представлены интересы разных сторон Java Community.
Идея новой модульной системы в Java была несколько раз отвергнута JCP (что вообще случается довольно редко), но всё-таки вошла в язык после значительных доработок и послаблений. Тем не менее спустя несколько лет мы видим, что идея так и не пришлась по вкусу Java-разработчикам и модулями пользуются редко.
Дженерики
Дженерики в Java, в отличие от многих других языков, — стираемые. Это означает, что все типовые параметры стираются через javac до Object. В рантайме вы не сможете понять, какой тип вам на самом деле пришёл. У этого подхода есть свои плюсы и минусы, а споры о том, что лучше — стираемые или нестираемые дженерики — продолжаются и по сей день. Тем не менее, выбор в пользу стираемых дженериков в Java сделан не случайно. Подробное обоснование, почему дженерики в Java такие, можно почитать вот в этой статье Брайана Гетца «Background: how we got the generics we have».
Но как бы вы не относились к реализации дженериков в Java, сейчас есть существенная проблема: нельзя использовать примитивные типы в качестве типовых параметров (ведь они несовместимы с Object). Это сокращает выразительную силу языка и вынуждает либо использовать Boxed-типы (что может ухудшить производительность), либо и вовсе специализировать функции под примитивные типы вручную. Исправить это собираются с помощью специализированных дженериков в рамках проекта Valhalla.
Обратная совместимость
Обратная совместимость — один из главных принципов Java, которым совет JCP не пожертвует даже ради самой передовой фичи. От некоторых вещей иногда отказываются, но при этом старый код всё равно должен продолжать работать.
Например, реализация исключений через инструкции JSR/RET встречается в старом байт-коде, а в новом — нет. Но до сих пор во всех тулах нужно уметь поддерживать старый байт-код ради библиотеки, написанной 20 лет назад, но которая так и продолжает использоваться в современных проектах в виде того же старого формата .jar.
С одной стороны, этот принцип позволяет построить стабильную экосистему. Бизнес может не бояться, что выйдет новая версия, которая всё сломает и навсегда заблокирует путь к обновлению. С другой стороны, это тормозит инновации. Возможно, Java развивается медленнее, чем C#, потому, что у нас обратная совместимость — это священная корова, которую нельзя трогать.
Насколько глубоко разработчику нужно знать JVM
На самом деле это очень холиварный вопрос. В основном ответ на него зависит от задач, которые хочет решать джавист.
Думаю, 30% программистов могут спокойно работать и не понимать, что происходит внутри виртуальной машины. Другим 50% достаточно на среднем уровне знать, как устроена JVM, — тогда они будут отличными мидлами и сеньорами. Их работа не в том, чтобы копаться в кишках виртуальной машины, но некоторые знания о её устройстве помогут им писать более эффективный код и избежать многих ошибок. В оставшихся 20% задач без знания деталей всё-таки не обойтись. Получается, чем лучше вы разбираетесь в виртуальной машине, тем более широкий класс задач способны решать.

Если в проекте важна производительность и повысить её с помощью выбора более оптимального алгоритма нельзя, надо разбираться, как работает сборщик мусора и как повысить его эффективность, какие настройки можно поменять, что такое деоптимизация, почему она происходит и так далее.
Java-разработчик, который регулярно погружается в эту тему, со временем сможет стать Performance Engineer. Эти специалисты находят проблемы производительности, анализируют глубинные процессы в JVM и исправляют их. Они решают проблемы не дописыванием кода в виртуальную машину, а настраивают её, как часовщики.
С точки зрения культуры всем разработчикам на managed-языках полезно знать, что происходит под капотом машины, когда они нажимают кнопку Run в IDE. Конечно, не нужно запоминать, что и в какой момент делает каждая деталь JVM. Но пройти вводный курс и понять на концептуальном уровне, что такое интерпретатор, Just In Time Compiler, что значит «умер объект» и так далее, — полезно.
Разработчик, который не знает ответов на все эти вопросы, способен решить довольно много задач. Но однажды он столкнётся с ошибками, причины которых даже не сможет понять. Например, если не задумываться, откуда берётся память и как она возвращается операционной системе, то можно получить от IDE сообщение Out of memory. А всё потому, что программа потребляет слишком много памяти. Разработчик, который знает, как устроена память, постарается оптимизировать код и не создавать горы объектов.
Куда развивается JVM
Сейчас довольно активно развиваются три проекта, которые рано или поздно войдут в новые версии Java.
Loom
Loom — это проект по добавлению виртуальных тредов в Java. С ним треды не мапятся на треды операционной системы, а живут сами по себе. И лишь когда их нужно исполнить, они будут садиться на треды операционки.
Раньше каждому экземпляру класса java.lang.Thread (через которые реализуется многопоточное исполнение) соответствовал один поток операционной системы. Вполне логичное решение, но есть проблема: потоков операционной системы не слишком много, поэтому вряд ли у вас получится создать больше нескольких тысяч потоков, реализованных таким образом. Project Loom предлагает альтернативу — виртуальные потоки, которые уже не связаны соотношением 1 к 1 с потоками виртуальной системы. Вместо этого они время от времени садятся на эти OS threads, чтобы выполнить очередной квант работы.
Идея не новая. Например, в Go так было всегда. Да и вообще о виртуальных тредах говорили уже много десятилетий. И даже в самом Java в 1990-е были грин-треды — схожая идея, где N зелёных потоков назначались на один поток операционной системы (тогда в процессорах было по одному ядру и проблема легковесной многопоточности не стояла так остро). Можно сказать, что Loom — это в некотором смысле реинкарнация зелёных тредов, но сделанная на более серьёзном уровне.
Этот проект очень важен, так как в современном мире микросервисов и серверов с огромным количеством одновременных подключений вам часто хотелось бы создавать десятки и сотни тысяч тредов. Со старым подходом к реализации через OS threads это просто невозможно, а вот с Loom всё получится.
Panama
Panama — это проект, посвящённый взаимодействию между Java и нативным кодом, написанным, например, на C или C++. Конечно, Java мог взаимодействовать с нативным кодом с самого рождения. Основное решение в этой области — Java Native Interface, очень мощный, но медленный и неудобный в использовании механизм. Чтобы взаимодействовать с нативами, приходилось писать некрасивый, громоздкий и, что самое главное, небезопасный код. Я уверен, что любому джависту, который хоть что-нибудь делал с JNI, как минимум не понравилось, а как максимум — он до сих видит кошмары про развалы в JNI.
Чтобы избавить Java-разработчиков от ночных кошмаров, авторы Panama уже много лет работают над своим проектом, который, как Панамский канал, соединяет два мира: нативный и Java. Panama предлагает новый интерфейс и его оптимизированную реализацию на стороне JVM, которая позволит приятно и безопасно работать с нативным кодом, в том числе с off-heap .
Valhalla
Проект Valhalla посвящён введению в Java новых типов объектов, так называемых value-типов, или inline-типов, или же primitive-value-типов — это текущее название. Да, проект настолько сложный, что даже основная его концепция часто меняет имя!
Если коротко, то смысл в том, чтобы выделить подкласс объектов, у которых нет identity. Что это такое? В Java вы можете создать два абсолютно одинаковых объекта с помощью оператора new. Потом сравнить их на == и получить false. Это разные объекты! Они сравниваются не по содержимому (которое одинаковое), а по некоторому уникальному свойству, назначаемому каждому объекту при создании. Вот возможность сравнивать не по содержимому, а по чему-то ещё — это и есть identity.
С другой стороны, можно объявить две переменные типа int, записать в каждую из них значение 42, сравнить на == и получить результат true. Всё потому, что у int никакого identity нет, они сравниваются по содержимому.
Цель Valhalla — дать пользователям возможность самим описывать классы, у которых нет identity, и подготовить к этому уже существующую типовую систему. Как следствие, можно будет, например, заводить плоские поля таких типов внутри других классов (то есть фактически инлайнить их поля внутрь других объектов), работать с плоскими массивами таких объектов и даже получить специализированные дженерики.
Это огромное, фундаментальное изменение всей платформы Java, за которым очень интересно следить и которого стоит ожидать, ведь оно принесёт множество возможностей обычным разработчикам.
На каких языках разрабатывают JVM и библиотеки для Java
Сами виртуальные машины Java обычно пишут на С++, стандартная библиотека же в основном написана на самом Java (хотя иногда и приходится уходить в нативный код, то есть снова в плюсы). Вообще, так повелось, что системщину пишут на плюсах, ведь здесь, с одной стороны, важна производительность, а с другой — связь с низким уровнем, с операционной системой и железом. В то же время стало очевидно, что далеко не все компоненты виртуальной машины обязательно нужно писать на С++. Например, зачем делать компилятор на С++, если есть замечательные managed-языки вроде Java или Scala?
Вот, например, в Oracle Labs разрабатывает проект GraalVM. Он состоит из трёх компонентов:
- КомпиляторGraal — мощный оптимизирующий компилятор, в будущем он, возможно, заменит компилятор C2 из HotSpot. Он не уступает по качеству многих оптимизаций, но написан полностью на Java.
- Truffle Framework — специальный фреймворк для написания интерпретаторов других языков, которые потом запускаются на GraalVM. По сути, это быстрый способ реализовать любой язык программирования на базе GraalVM! Опять-таки, интерпретаторы пишутся на managed-языках.
- Graal Native Image — отдельный режим сборки приложений, который на самом деле представляет из себя целую виртуальную машину, но написанную с некоторыми нарушениями спецификации. Зато она позволяет получить быстрый стартап и низкое потребление памяти. Рантайм для такой виртуальной машины тоже частично написан на Java! Понятно, что не полностью и с разными ограничениями языка (на самом деле там используется отдельный диалект — System Java), но тем не менее это возможно.
Вообще, писать виртуальные машины на managed-языках — давняя мечта системщиков. Ведь, например, компилятор С написан на С? Вот и разработчики виртуальных машин для managed-языков хотят писать их на Java или других managed-языках, а не на опасном и болезненном С++.
Как контрибьютить в OpenJDK
Для начала скажу, что я никогда не контрибьютил в OpenJDK, а занимался разработкой другой, проприетарной и закрытой JVM. Поэтому о процедуре коммита в OpenJDK имею только смутное представление, так что написанному ниже доверяйте на свой страх и риск!
Если бы я задался целью контрибьютить в OpenJDK, то думал бы в двух направлениях:
- Найти что-то небольшое, что получится исправить или улучшить. Это может быть несложный (и низкоприоритетный) баг в баг-трекере OpenJDK или неэффективность какого-нибудь алгоритма в платформенных классах. Исправьте эту проблему, соберите OpenJDK и проверьте, что, например, алгоритм и правда стал работать лучше на каких-нибудь бенчмарках.
Потом напишите в мейлинг-лист, расскажите о сути улучшения и попросите сделать ревью. Наконец, пройдите его, а затем и остальную процедуру коммита. Насколько я знаю, она довольно стандартная, ведь OpenJDK теперь находится на GitHub. Поздравляю, теперь вы контрибьютор в OpenJDK!
По рассказам людей, которые прошли по этому пути, вся процедура может занять довольно много времени, особенно в первый раз, но в целом это вполне реально.
- Устроиться на работу в компанию-контрибьютор. В этом случае вы будете разрабатывать код, который попадёт в OpenJDK, в рабочее время. Однако сложность в том, что таких компаний всё-таки очень мало. Зато попасть в одну из них — действительно круто. Яркий пример такой компании — питерская BellSoft, которая в прошлом году заняла четвёртое место в мире (!) среди внешних (не из Oracle) контрибьюторов в OpenJDK.
На кого подписаться и где следить за новостями JVM
Разработчики виртуальных машин регулярно улучшают свои VM и зачастую делятся новостями о разработке и состоянии тех или иных проектов в рамках OpenJDK и не только. Черпать актуальную информацию можно из мейлинг-листов, конференций и Twitter.
Мейлинг-листы
Мейлинг-лист — это такой почтовый ящик, на который можно подписаться. Подписчики мейлинг-листа получают письма из общей рассылки, но ещё могут отвечать, участвовать в дискуссиях или задавать вопросы. Именно так и общаются разработчики многих языков программирования.
Мейлинг-листов про Java и JVM довольно много, есть свои мейлинг-листы для разных компонентов JDK и разных подпроектов в OpenJDK. Например, есть отдельные мейлинг-листы для упомянутых выше проектов Loom или Valhalla. Так что тут главное — выбрать темы, которые вам интересны, а потом найти соответствующие мейлинг-листы на сайте сообщества Java.
Новые предложения по технологии в первую очередь пишут и обсуждают именно в мейлинг-листах, поэтому в них отражается история развития Java и JVM. Такой канал связи действительно работает, хоть и может показаться немного устаревшим. Если хотите узнать сложные детали, например как работает конкретная часть виртуальной машины, советую подписаться на соответствующий мейлинг-лист.
Конференции
Более привычный и простой для восприятия источник информации — конференции. Разработчики JVM регулярно выступают с докладами, в которых по косточкам разбирают детали реализации, при этом в доступной и понятной форме.
Наибольшая концентрация знаний — на ежегодной конференции Java Virtual Machine Language Summit. Видео выступлений можно посмотреть на YouTube-канале Java. Очень много хардкорных и качественных докладов от разработчиков JVM есть на российских конференциях Joker и JPoint от компании JUG Ru Group.
Сами мы (JUGNsk) тоже организуем свою Java-конференцию SnowOne в Новосибирске. В нашем программном комитете много системщиков, поэтому докладам о внутренностях виртуальных машин на конференции уделяется особое внимание.
Ещё один источник свежей информации — Twitter. Часто там появляются последние новости о Java вообще и JVM в частности. Из англоязычных авторов советую подписаться на аккаунты Брайана Гетца и Марка Рейнхольда — архитекторов языка Java и Java-платформы.
В России тоже есть суперзвёзды и разработчики JVM, например Алексей Шипилёв. Он один из главных контрибьюторов Shenandoah GC, работал в Oracle, а сейчас — в Red Hat. При этом он пишет классные посты (чего стоит одна только серия JVM Anatomy Quarks!) и выступает с отличными докладами о том, как устроена JVM изнутри.
Ещё всегда интересно читать Сергея Куксенко — перформанс-инженера в Oracle. Он разгоняет многие фичи, которые появляются в Java, и интересно об этом рассказывает. А новости о HotSpot и Panama можно найти в аккаунте Владимира Иванова.
В целом в Twitter есть ещё огромное количество как русскоязычных, так и англоязычных крутых инженеров, связанных с разработкой JVM. Так что принцип здесь такой же, как с мейлинг-листами: находите человека, участвующего в определённом проекте в OpenJDK (или за его пределами), и смело подписываетесь!
Резюме:
- Чтобы решать рутинные задачи, джависту не нужно разбираться в деталях JVM. Но перейти на следующий уровень не получится, если хотя бы на концептуальном уровне не понимать, как работает интерпретатор, что такое Just In Time Compilation, как устроена память и так далее.
- JDK — это комплект инструментов Java-разработчика. В него входит: javac, сама виртуальная машина, стандартная библиотека классов и документация. OpenJDK — это эталонная версия JDK с полностью открытым исходным кодом. Основные контрибьюторы в этот проект — это Oracle, но кроме неё есть и много других компаний и разработчиков, которые вносят свой вклад.
- Обычно виртуальные машины пишут на языках C/C++. Но уже есть проекты, в которых JVM, компилятор и рантайм почти полностью написаны на Java или его диалекте.
- Контрибьютить в OpenJDK может любой желающий, но серьёзные изменения в JVM обычно вносят опытные разработчики. Тем не менее возможно найти себе небольшие задачи, с которых и начать этот путь.
Читайте также:
- Чем Rust отличается от плюсов: откровение ветерана C++
- Есть ли место творчеству в программировании: подборка мнений опытных разработчиков
- Дженерики в Java для тех, кто постарше: стирание типов, наследование и принцип PECS