Разрыв между «написать» и «запустить»
Вы пишете код и запускаете его. Между двумя этими процессами происходит нечто необычное и незаметное — семь этапов конвейера, о котором большинство инженеров даже не подозревают. Это и есть JVM (Java Virtual Machine — виртуальная машина Java).
JVM не просто исполняет байт-код (промежуточное представление программы, не зависящее от процессора). Она его оптимизирует и управляет памятью, ускоряясь по мере работы. Ни одна другая стандартная среда исполнения не делает этого по умолчанию.

Этап 1: Сборка (Build)
Пишем исходный код с расширением .java. Запускаем javac (компилятор Java). Результат — не машинный код, а байт-код: компактный, платформонезависимый формат, хранящийся в файлах .class, JAR-файлах (Java Archive — архив Java) или модулях.
// HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, JVM");
}
}
javac HelloWorld.java # создает класс HelloWorld.class
javap -c HelloWorld # проверяет байт-код
Байт-код не привязан ни к какому типу центрального процессора. В этом и заключается смысл принципа «напиши один раз — запускай где угодно» — это гарантия именно байт-кода, а не конкретного языка.
Этап 2: Загрузка (Load)
Классы загружаются лениво, то есть только в момент первого обращения во время выполнения, а не все сразу при запуске. JVM использует три загрузчика, образующих строгую иерархию с делегированием родителю:
Bootstrap ClassLoader ← loads core JDK (java.lang, etc.)
└── Platform ClassLoader ← extensions, javax.*
└── System ClassLoader ← your application code
Эта иерархия предотвращает подмену системных классов вредоносным кодом. Невозможно внедрить поддельный java.lang.String в обход загрузчика начального уровня.
Этап 3: Линковка (Link)
Линковка включает три стадии, которые часто объединяют одним словом:
- Верификация (Verify) — байт-код проверяется на соответствие спецификации. Недопустимые переходы, нарушения типов отсутствуют. Именно это делает исполнение непроверенного байт-кода безопасным.
- Подготовка (Prepare) — выделяется память под статические поля и инициализируется значениями по умолчанию (
0,null,false). - Разрешение (Resolve) — символьные ссылки (имена классов в виде строк) преобразуются в прямые указатели на область памяти.
Этап 4: Инициализация (Initialize)
Теперь статические поля получают свои реальные значения, а статические блоки выполняются ровно один раз, при первом использовании класса.
class Config {
static final int TIMEOUT;
static {
TIMEOUT = Integer.parseInt(System.getenv("TIMEOUT_MS"));
System.out.println("Config loaded");
}
}
JVM гарантирует, что этот код выполнится однократно и потокобезопасно без дополнительных блокировок.
Этап 5: Управление памятью (Memory)
Здесь архитектура JVM кардинально отличается от большинства сред исполнения. Память разделяется в зависимости от характера доступа:
| Область (Region) | Область видимости (Scope) | Содержимое (Contents) |
| Куча (Heap) | Общая для всех потоков | Все объекты |
| Область методов (Method Area) | Общая для всех потоков | Метаданные классов, байт-код |
| Стек (Stack) | На поток | Фреймы, локальные переменные |
| Счетчик команд (PC Register) | На поток | Указатель на текущую исполняемую инструкцию |
| Стек для нативных методов (Native Stack) | На поток | Вызовы нативных (Native) методов (написанных на C/C++ и вызываемых через JNI) |
Сборщик мусора (Garbage Collector, GC) работает с кучей, автоматически освобождая память недостижимых объектов. Современные сборщики мусора, такие как G1 (Garbage-First) и ZGC (Z Garbage Collector), выполняют сборку с паузами менее одной миллисекунды, при этом разработчику не нужно вызыватьfree().
Этап 6: Исполнение (Execute)
Вот главный инженерный шедевр.
JVM начинает с интерпретации байт-кода — медленно, но без задержек на старте. Параллельно она профилирует наиболее часто вызываемые методы. Эти «горячие» пути компилируются в нативный машинный код JIT-компилятором (Just-In-Time — точно в определенное время) и кэшируются.
Cold path: bytecode → interpreter (slow, no warmup needed)
Hot path: bytecode → JIT → native machine code (fast, after profiling)
Результат: программа на Java работает быстрее после того, как она уже некоторое время выполнялась. JVM обучается в реальном времени, подстраиваясь под вашу нагрузку.
JNI (Java Native Interface — интерфейс для вызова нативных методов) обрабатывает пограничный случай, когда необходимо вызвать код на C/C++ из Java или наоборот.
Почему эта архитектура до сих пор актуальна
У других языков были десятилетия, чтобы скопировать эту модель. Но мало какой из них воспользовался этой возможностью. Причины, по которым эта архитектура остается востребованной:
- Платформонезависимость через байт-код оказывается проще кросскомпиляции.
- Сборщик мусора устраняет целый класс ошибок (висячие указатели, использование после освобождения памяти).
- Профилирование в JIT позволяет выполнять оптимизации, недоступные статическим компиляторам, поскольку JVM видит реальные данные времени выполнения.
- Механизм загрузки классов обеспечивает возможность создания плагинов, горячей перезагрузки и изолированного исполнения.
JVM также нативно выполняет Kotlin, Scala, Clojure и Groovy. Все они компилируются в один и тот же байт-код. Среде исполнения нет дела до того, на каком языке написан код.
Вывод
JVM — это не громоздкая устаревшая среда исполнения. Это непрерывно оптимизирующий движок выполнения, который за счет небольших затрат на запуск обеспечивает производительность, превосходящую C, в стабильном режиме работы долгосрочных сервисов.
В следующий раз, когда Java-сервис будет обрабатывать миллионы запросов без перезапуска, знайте: это JIT-компилятор и сборщик мусора работают незаметно, в фоновом режиме — именно так, как было задумано.
Читайте также:
- Байт-код JVM: манипулирование и инструментация
- Сборка мусора в Java: что это такое и как работает в JVM
- JAVA: разница между параметрами JVM -D, -X, -XX
Читайте нас в Telegram, VK и Дзен
Перевод статьи The Latency Gambler: JVM Is the Most Underrated Engineering Marvel in Software





