Архитектура JVM в Java
Структура JVM: загрузчик классов, области данных времени выполнения, механизм исполнения и нативный интерфейс.
Java Virtual Machine (JVM) — это программа, которая запускает вашу программу. Вы компилируете исходный код .java в платформо-независимый байткод .class, а JVM загружает этот байткод, проверяет его, размещает объекты в памяти и выполняет их на реальном железе. "Написать однажды, запускать везде" — это в действительности "скомпилировать однажды, а дальше пусть JVM каждой платформы делает всё остальное." Эта глава описывает внутреннюю структуру JVM — три подсистемы, которые есть в каждой реализации, — чтобы главы о модели памяти и сборке мусора имели готовую основу.
Три подсистемы
JVM, от любого поставщика, организована в три взаимодействующие подсистемы. Всё остальное — детали внутри одной из них.
| Подсистема | Ответственность |
|---|---|
| Загрузчик классов | Находит, загружает, компонует и инициализирует файлы .class в среду выполнения |
| Области данных времени выполнения | Память, которой управляет JVM: куча, стеки, область методов, PC-регистры |
| Механизм исполнения | Интерпретирует и JIT-компилирует байткод, а также запускает сборщик мусора |
Загрузчик классов вносит типы внутрь, области данных хранят состояние, а механизм исполнения выполняет код. Вызов метода задействует все три: его класс загружается, его фрейм помещается на стек, а его байткод выполняется.
Подсистема загрузчика классов
Классы загружаются не все сразу при запуске. JVM загружает класс лениво, при первом обращении к нему, в три фазы: загрузка (чтение байтов), компоновка (верификация байткода, подготовка статических полей, разрешение ссылок) и инициализация (выполнение статических инициализаторов и блоков static { }).
Загрузчики формируют иерархию с приоритетом родителя. Когда загрузчику нужен класс, он сначала делегирует вверх к родителю и только потом пробует сам — поэтому основной тип, например String, всегда поступает из доверенного bootstrap-загрузчика и никогда не может быть перекрыт кодом приложения.
// Walk the loader chain of any class
ClassLoader loader = MyType.class.getClassLoader();
while (loader != null) {
System.out.println(loader.getName());
loader = loader.getParent();
}
// A null result means the bootstrap loader (native, no Java object).
System.out.println(String.class.getClassLoader()); // prints: nullТри стандартных загрузчика, от дочернего к родительскому: application (загрузчик класспаса), platform-загрузчик (модули JDK, например java.sql) и bootstrap-загрузчик (ядро java.base, реализованное в нативном коде и представленное как null). Глава о загрузке классов подробно рассматривает этот жизненный цикл и модель делегирования; глава о модулях объясняет, как упакованы java.base и остальные.
Области данных времени выполнения
Как только класс загружен, его код и данные находятся в регионах, которые JVM делит по назначению:
- Куча (Heap) — общая для всех потоков; здесь живут все объекты и массивы. Именно ей управляет сборщик мусора.
- Стеки JVM — по одному на поток. Каждый вызов метода помещает на стек фрейм с локальными переменными и операндами; при возврате фрейм снимается. Глубокая рекурсия переполняет стек (
StackOverflowError). - Область методов (Metaspace в современных JVM) — метаданные классов, пул констант времени выполнения и статические поля. Не входит в кучу.
- PC-регистр — на каждый поток; адрес байткодовой инструкции, выполняемой в данный момент.
// Heap allocation: 'new' carves space out of the heap
byte[] buffer = new byte[1024]; // lives on the heap, GC-managed
// Stack growth: each call adds a frame to this thread's stack
static long factorial(long n) {
return n <= 1 ? 1 : n * factorial(n - 1); // each call = one more frame
}Ключевое разделение — куча против не-кучи: экземпляры объектов находятся в куче; структуры классов и сама машинерия JVM — нет. Глава о стеке и куче подробно сравнивает эти два региона.
Механизм исполнения
Механизм исполнения превращает байткод в действие. Современные JVM HotSpot адаптивны: они начинают с интерпретации байткода (быстрый старт), профилируют горячие методы, а затем передают их JIT-компилятору (Just-In-Time), который генерирует оптимизированный нативный машинный код. Холодный код остаётся интерпретируемым; горячий — компилируется: вы платите за оптимизацию только там, где она себя окупает.
В механизм исполнения также входит сборщик мусора, освобождающий объекты кучи, до которых больше нет ни одной живой ссылки, и Java Native Interface (JNI) — мост к библиотекам, написанным на C/C++.
// A hot loop: the JIT will compile sum() to native code after enough calls
long total = 0;
for (int i = 0; i < 100_000_000; i++) {
total += i; // interpreted at first, then JIT-compiled, then much faster
}Практический пример: инспекция работающей JVM
Эта программа ничего не настраивает — она запрашивает у работающей JVM описание самой себя через бины java.lang.management и API загрузчика классов. Она выводит имя VM и механизма исполнения, обходит иерархию загрузчиков, сообщает об использовании памяти кучи и не-кучи, а также растит стек с помощью рекурсии.
Что стоит отметить по результатам запуска:
RuntimeMXBeanназывает механизм исполнения — JVM семейства 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. Делегирование с приоритетом родителя работает.- Строки памяти показывают использование кучи в KB, но максимум кучи в MB и отдельный показатель не-кучи: экземпляры объектов находятся в управляемой GC куче, тогда как метаданные классов (Metaspace) считаются не-кучей — оба региона отслеживаются независимо.
depth(1)возвращает5, потому что каждый рекурсивный вызов помещал собственный фрейм на стек JVM текущего потока и снимал его при возврате; стек принадлежит потоку и имеет фреймовую структуру, поэтому неограниченная рекурсия заканчиваетсяStackOverflowError, а не порчей кучи.