Архитектура JVM в Java
Как устроена JVM — загрузчик классов, области данных времени выполнения, движок исполнения и нативный интерфейс.
Архитектура JVM в Java
Виртуальная машина Java — это программа, которая запускает вашу программу. Вы компилируете исходный код .java в платформонезависимый байт-код .class, а JVM — это то, что загружает этот байт-код, верифицирует его, размещает его объекты в памяти и исполняет его на реальном оборудовании. «Write once, run anywhere» на самом деле означает «скомпилируй один раз, а остальное пусть делает JVM каждой платформы». Эта глава картирует внутреннюю структуру JVM — три подсистемы, общие для каждой реализации, — чтобы у следующих глав о памяти и сборке мусора был каркас, на который можно опереться.
Три подсистемы
JVM, от какого бы поставщика она ни была, организована в три взаимодействующие подсистемы. Всё остальное — это детали внутри одной из них.
| Подсистема | Ответственность |
|---|---|
| Загрузчик классов | Находит, загружает, связывает и инициализирует файлы .class в среде выполнения |
| Области данных времени выполнения | Память, которой управляет JVM: куча, стеки, область методов, регистры PC |
| Движок исполнения | Интерпретирует и JIT-компилирует байт-код и запускает сборщик мусора |
Загрузчик классов вносит типы внутрь, области данных времени выполнения хранят состояние, а движок исполнения исполняет код. Вызов метода затрагивает все три: его класс загружается, его кадр кладётся на стек, а его байт-код исполняется.
Подсистема загрузчика классов
Не все классы загружаются при запуске. JVM загружает класс лениво, при первом обращении к нему, в три фазы: загрузка (чтение байтов), связывание (верификация байт-кода, подготовка статических полей, разрешение ссылок) и инициализация (выполнение статических инициализаторов и блоков static { }).
Загрузчики образуют иерархию «сначала родитель». Когда у загрузчика запрашивают класс, он делегирует вверх своему родителю, прежде чем попробовать сам — так что базовый тип вроде String всегда поступает от доверенного загрузчика bootstrap и никогда не может быть затенён кодом приложения.
// Пройти по цепочке загрузчиков любого класса
ClassLoader loader = MyType.class.getClassLoader();
while (loader != null) {
System.out.println(loader.getName());
loader = loader.getParent();
}
// Результат null означает загрузчик bootstrap (нативный, без Java-объекта).
System.out.println(String.class.getClassLoader()); // печатает: nullТри стандартных загрузчика, от потомка к родителю, — это загрузчик приложения (classpath), загрузчик платформы (модули JDK вроде java.sql) и загрузчик bootstrap (ядро java.base, реализованное в нативном коде, представленное как null).
Области данных времени выполнения
После загрузки класса его код и данные живут в областях, которые JVM разбивает для разных целей:
- Куча (heap) — разделяется между всеми потоками; здесь живут каждый объект и каждый массив. Это то, чем управляет сборщик мусора.
- Стеки JVM — по одному на поток. Каждый вызов метода кладёт кадр, содержащий локальные переменные и операнды; кадр снимается при возврате. Глубокая рекурсия переполняет его (
StackOverflowError). - Область методов (Metaspace в современных JVM) — метаданные классов, пул констант времени выполнения и статические поля. Память вне кучи.
- Регистр PC — на поток; адрес исполняемой в данный момент инструкции байт-кода.
// Аллокация в куче: 'new' вырезает место из кучи
byte[] buffer = new byte[1024]; // живёт в куче, под управлением GC
// Рост стека: каждый вызов добавляет кадр в стек этого потока
static long factorial(long n) {
return n <= 1 ? 1 : n * factorial(n - 1); // каждый вызов = ещё один кадр
}Куча против не-кучи — ключевое разделение: экземпляры объектов находятся в куче; структуры классов и сама машинерия — нет.
Движок исполнения
Движок исполнения превращает байт-код в действие. Современные JVM HotSpot адаптивны: они начинают с интерпретации байт-кода (быстрый старт), профилируют, какие методы исполняются горячо, затем передают их компилятору Just-In-Time (JIT), который выдаёт оптимизированный нативный машинный код. Холодный код остаётся интерпретируемым; горячий код компилируется — вы платите за оптимизацию только там, где она окупается.
В движке также размещаются сборщик мусора, который освобождает объекты кучи, более не достижимые ни по одной живой ссылке, и Java Native Interface (JNI) — мост к библиотекам, написанным на C/C++.
// Горячий цикл: JIT скомпилирует sum() в нативный код после достаточного числа вызовов
long total = 0;
for (int i = 0; i < 100_000_000; i++) {
total += i; // сначала интерпретируется, затем JIT-компилируется, затем намного быстрее
}Проработанный пример: инспекция живой JVM
Эта программа ничего не настраивает — она просит работающую JVM описать себя через бины java.lang.management и API загрузчика классов. Она называет VM и движок исполнения, проходит по иерархии загрузчиков, сообщает память кучи против не-кучи и наращивает стек рекурсией.
Что вынести из запуска:
RuntimeMXBeanназывает движок исполнения — VM семейства HotSpot (строкаvm nameпоказывает что-то вроде «OpenJDK 64-Bit Server VM») — подтверждая, что ваш байт-код исполняет JIT-способный движок «Server», а не голый интерпретатор.- Цепочка загрузчиков печатает что-то вроде
<unnamed> -> app -> platform -> bootstrap(null): каждый шаг цикла поднимался к родителю, и цепочка завершиласьnull-родителем — загрузчиком bootstrap, который является нативным кодом без Java-объекта. Иерархия реальна и наблюдаема, а не метафора. String.class.getClassLoader()равноnull— базовые типыjava.baseпоступают от того же загрузчика bootstrap на вершине цепочки, и именно поэтому код приложения никогда не может подменить собственнуюString. Делегирование «сначала родитель» делает своё дело.- Строки памяти показывают использованную кучу в КБ, но максимум кучи в МБ, а также отдельную цифру не-кучи: экземпляры объектов живут в управляемой GC куче, тогда как метаданные классов (Metaspace) учитываются как не-куча — две области отслеживаются независимо.
depth(1)возвращает5, потому что каждый рекурсивный вызов положил собственный кадр на стек JVM этого потока и снял его при возврате; стек существует на поток и структурирован кадрами, поэтому неуправляемая рекурсия заканчиваетсяStackOverflowError, а не порчей кучи.
Практика
Программа Java ссылается на класс 'java.lang.String'. В стандартной иерархии загрузчиков «сначала родитель» какой загрузчик в конечном счёте предоставляет его и как этот загрузчик выглядит из кода Java?