Прямо сейчас идет день открытых дверей по курсу «Team Lead 2.0». Присоединяйтесь!

Виртуальная машина Java — что это?

Все мы знакомы с понятием JVM (Java Virtual Machine). В статье мы рассмотрим, что же такое виртуальная машина Java, из чего состоит, как работает, какую имеет архитектуру. Для лучшего понимания этого вы вкратце узнаете, как устроен class-файл и каким образом виртуальная машин Java выполняет обработку и исполнение байт-кода.

JVM — это ключевой компонент платформы Java («Джава», «Ява»), который загружает, проверяет и выполняет код. Давайте вспомним, что главной задачей Java-разработчиков было создание переносимых приложений. И JVM в данном случае играет основную роль с точки зрения переносимости, так как именно она обеспечивает необходимый уровень абстракции между скомпилированной Java-программой, базовой аппаратной платформой и ОС. При этом, несмотря на такой дополнительный «слой», скорость работы приложений в Java является довольно высокой, так как байт-код, выполняемый JVM, да и она сама прекрасно оптимизированы.

Структура class-файла в Java VM

Давайте напишем простейшее приложение, а потом скомпилируем его. При этом компилятор создаст файл .class, поместив туда информацию о нашем приложении для Java VM. И что же мы увидим внутри? А увидим мы файл, поделенный на 10 секций, причём последовательность этих секций будет строго задана и будет определять всю структуру class-файла.

Начинается файл со стартового числа 0xCAFEBABE. Это число есть в каждом классе, и оно является обязательным флагом для Java Virtual Machine. Благодаря ему система понимает, что перед ней находится class-файл.

1-20219-48164d.jpg Последующие 4 байта class-файла включают младший и старший номера Java-версий. По сути, они идентифицируют версию формата определённого class-файла, позволяя Java VM проверять, возможна ли по отношению к нему поддержка и загрузка. При этом каждая Java VM содержит ограничение версии, которую может загрузить (более поздние версии игнорируются).

С 9-го байта идёт пул констант. В нём содержатся все константы, относящиеся к нашему классу. В каждом классе их бывает разное количество, поэтому перед массивом есть переменная, которая указывает на длину (по сути, пул констант — это массив переменной длины). А каждая константа занимает 1 элемент в массиве.

Идём дальше. Во всём class-файле константы обозначаются целочисленным индексом, а он указывает их положение в массиве. Начальной константе соответствует индекс 1, второй — 2 и т. п.

Каждый элемент из пула констант начинается с 1-байтового тега, который определяет его тип. Всё это позволяет Java VM понять, каким образом правильно обработать далее идущую константу. Для этого зарезервировано 14 типов констант.

2-20219-c62095.jpg К примеру, когда тег указывает, что константа есть строка, Java VM получает значение тега № 1, обрабатывая число, следующее за тегом, как длину массива байт, которые нужно считать для получения нужной нам строки.

А прочитав блок с константами, Java VM перейдёт к следующим 2-м байтам (флагам доступа), определяющим, описывает данный файл класс либо интерфейс, является ли класс финальным.

При этом имена класса, а также имена его родительского класса сохраняются в массиве констант, и на них указывают последующие четыре байта в файле.
Чуть по-другому обстоит дело с интерфейсами. Мы знаем, что класс наследует одновременно от множества интерфейсов, поэтому хранить нужно массив ссылок на пул констант. Таким образом, за определением класса, а также его родительского класса идёт число, которое характеризует размер массива интерфейсов, плюс сам массив. И все возможные значения кодов вы можете увидеть в таблице ниже: 3-20219-3e1e0b.jpg Подобную структуру имеет и очередной блок — Fields. Он начинается с 2-байтового параметра числа полей в данном интерфейсе либо классе. Далее следует массив структур переменной длины. И каждая структура включает данные об одном поле: тип, значение, имя поля. В списке отображены лишь те поля, которые объявлены классом либо интерфейсом, который определён в файле. А вот поля имплементированных интерфейсов и родительских классов здесь не присутствуют, так как задаются они в своих class-файлах.

Пришла пора поговорить о методах класса. Именно в них содержится вся логика любого приложения и весь исполняемый байт-код.

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

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

Загрузка классов

Разобравшись со структурой файла, глянем, как Java VM обрабатывает его. Чтобы попасть в Java VM, класс нужно сначала загрузить. Для этого есть классы-загрузчики: 1. Bootstrap. Это базовый загрузчик, загружающий платформенные классы. Родитель остальных классов и часть платформы. 2. Extension ClassLoader — потомок Bootstrap, загрузчик расширений. Загружает классы расширений, по умолчанию находящиеся в каталоге jre/lib/ext. 3. AppClassLoader — системный загрузчик классов из classpath. Непосредственный потомок Extension ClassLoader. Загружает классы из jar-файлов и каталогов, указанных CLASSPATH (переменной среды), системным свойством java.class.path либо параметром командной строки -classpath. 4. Собственный загрузчик — собственные загрузчики могут быть и у приложения.

В нашем случае главный класс приложения всегда грузится системным загрузчиком. Остальные — разными пользовательскими загрузчиками. Имя загрузчика в Java VM создаёт уникальное пространство имён, потому в приложении могут находиться несколько классов, имеющих одно и то же полное имя, если их обрабатывали разные загрузчики. Именно поэтому каждый загрузчик делегирует полномочия родителю. Таким образом, перед поиском класса для загрузки он сначала пытается «понять», не был ли нужный класс загружен раньше.

Линковка в Java VM

После загрузки класса в Java VM наступает этап линковки, и делится он на 3 части: 1. Верификация байт-кода. Статический анализ кода, выполняемый один раз для класса. Происходит проверка ошибок в байт-коде. Например, проверяется корректность инструкций, переполнение стека и совместимость типов переменных. 2. Выделение памяти под статические поля с последующей их инициализацией. 3. Разрешение символьных ссылок — Java VM подставляет ссылки на другие классы, поля и методы.

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

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

На данным момент в Java содержится больше 200 операций. Все коды занимают лишь 1 байт.

В основе работы Java VM — стек, т. к. основные инструкции работают именно с ним.

Приведём пример умножения 2 чисел: 4-20219-6f3539.jpg Операции выполняются во фрейме стека метода (в главном потоке исполнения приложения создаётся множество подстеков). 5-20219-f1173f.jpg В каждом стек-фрейме — массив локальных переменных.

Вызовы методов

В Java VM есть 2 главных вида методов: методы класса и методы экземпляра. Первые применяют статическое (раннее) связывание, вторые динамическое (позднее) связывание.

Java VM вызывает метод класса и выбирает его на основании типа ссылки на объект (всегда известен в процессе компиляции). С другой стороны, когда виртуальная машина Java вызывает метод экземпляра, происходит выбор метода для вызова на основании фактического класса объекта, а его можно узнать лишь при выполнении.

Именно для этого при вызове методов используются различные инструкции: invokevirtual и invokestatic. Эти функции ссылаются на запись в пуле констант в форме полного пути к нужной функции.

Java VM при этом снимает необходимое число переменных со стека, передавая в метод. Значение, возвращаемое методом, кладётся на стек. Типы возвращаемых значений методов: 6-20219-ea3124.jpg

Циклы

Последний нюанс — циклы. Смотрим, каким станет код, представленный ниже: 7-20219-2016ac.jpg Каждый раз на стеке появляются 2 числа, которые сравниваются. Если i > 9999, производится выход из цикла. Для создания цикла служит конструкция на основе goto.

Заключение

По умолчанию байт-код интерпретируется в большинстве Java VM. Но если система видит, что какой-нибудь код используется часто, подключается встроенный компилятор, компилирующий байт-коды в машинный код, что существенно ускоряет работу приложения.

Автор
0 комментариев
Для комментирования необходимо авторизоваться