Разрыв между «написать» и «запустить»

Вы пишете код и запускаете его. Между двумя этими процессами происходит нечто необычное и незаметное — семь этапов конвейера, о котором большинство инженеров даже не подозревают. Это и есть 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)

Линковка включает три стадии, которые часто объединяют одним словом:

  1. Верификация (Verify) — байт-код проверяется на соответствие спецификации. Недопустимые переходы, нарушения типов отсутствуют. Именно это делает исполнение непроверенного байт-кода безопасным.
  2. Подготовка (Prepare) — выделяется память под статические поля и инициализируется значениями по умолчанию  (0nullfalse).
  3. Разрешение (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-компилятор и сборщик мусора работают незаметно, в фоновом режиме — именно так, как было задумано.


Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи The Latency Gambler: JVM Is the Most Underrated Engineering Marvel in Software

Предыдущая статья7 библиотек Python для улучшения бэкенда