Виртуальная машина Java (JVM, Java Virtual Machine) — это среда исполнения, которая запускает Java-байткод независимо от процессора и операционной системы. Компилятор javac переводит исходный код не в машинные команды конкретного процессора, а в промежуточный байткод, а его уже исполняет JVM на нужной платформе.
Содержание
Ниже разберем цепочку «исходник -> байткод -> исполнение», три подсистемы JVM (загрузчик классов, механизм выполнения с JIT, сборщик мусора) и разведем понятия JVM, JRE и JDK, которые часто путают. Тему классов и объектов как таковых, а также обзор языка Java в этой статье не рассматриваем — только машину, которая исполняет код.
JVM vs JRE vs JDK: три разных понятия
Эти три сокращения обозначают разные вещи, и путаница здесь типична. Разведем их сразу таблицей, а дальше по тексту будем опираться на эти границы.
| Термин | Что это | Что входит | Кому нужно |
|---|---|---|---|
| JVM | Абстрактная машина (спецификация) и ее конкретная реализация, исполняющая байткод | Загрузчик классов, механизм выполнения, области памяти, GC | Нужна для любого запуска |
| JRE | Среда выполнения: JVM плюс стандартные библиотеки классов | JVM + библиотеки (java.base и другие) | Чтобы запускать готовые программы |
| JDK | Комплект разработчика: среда выполнения плюс инструменты | JRE + javac, javap, jar, jdb и другие |
Чтобы писать и собирать программы |
Важное уточнение: JVM — это одновременно и документ (спецификация JVM в составе Java SE), и программа, которая ей соответствует. Реализаций много; самая распространенная — HotSpot из проекта OpenJDK. Начиная с Java 11, отдельного standalone-дистрибутива JRE от Oracle нет: среду выполнения либо получают внутри JDK, либо собирают под приложение утилитой jlink.
От исходника к байткоду
Java-компиляция двухступенчатая. Сначала javac превращает текст в байткод и складывает его в .class-файлы. Байткод — это не машинный код процессора, а компактные инструкции для стековой виртуальной машины (iload, iadd, invokevirtual и подобные).
Возьмем минимальный полный пример и доведем его до наблюдаемого результата.
public class Hello {
public static void main(String[] args) {
int a = 2;
int b = 3;
System.out.println(a + b);
}
}
Компилируем и запускаем:
javac Hello.java
java Hello
Вывод:
5
Команда javac создала файл Hello.class — это и есть байткод. Заглянем внутрь дизассемблером javap, чтобы увидеть, что реально исполняет машина:
javap -c Hello
Фрагмент вывода (метод main) — он воспроизводится этой командой на любом актуальном JDK:
0: iconst_2
1: istore_1
2: iconst_3
3: istore_2
...
invokevirtual java/io/PrintStream.println
Инструкции iconst_2 и iconst_3 кладут числа на стек операндов, istore_1 и istore_2 сохраняют их в локальные переменные; перед сложением iload_1 и iload_2 (в полном листинге) возвращают значения на стек, где iadd их складывает, а invokevirtual вызывает метод. Именно эти команды, а не текст программы, и получает на вход JVM. Один и тот же Hello.class без перекомпиляции запустится на Windows, Linux и macOS — в этом суть переносимости.
Загрузчик классов: как байткод попадает в память
Первая подсистема JVM — загрузчик классов (class loader). Он находит .class-файлы, читает их в память и готовит к исполнению. Работа идет в три этапа:
- загрузка (loading) — чтение байткода класса в память;
- связывание (linking) — проверка корректности байткода (verification), подготовка статических полей, разрешение ссылок;
- инициализация (initialization) — выполнение статических инициализаторов и присваивание начальных значений.
Загрузчиков несколько, и они образуют иерархию с делегированием «сначала спрашиваем родителя»:
- bootstrap — загружает базовые классы платформы (модуль
java.base); - platform — загружает остальные классы платформенных модулей;
- application (system) — загружает классы вашего приложения с classpath или modulepath.
Загрузка ленивая: класс попадает в память не заранее, а в момент первого обращения к нему. Этап verification — это встроенная защита: JVM отказывается исполнять байткод, который нарушает правила типобезопасности или структуру стека, поэтому битый или подделанный .class не пройдет дальше проверки. Обратите внимание: имена platform-загрузчика актуальны для Java 9 и новее; в более старых версиях вместо него был extension class loader — это отличие стоит учитывать в legacy-коде.
Механизм выполнения и JIT-компиляция
Вторая подсистема — механизм выполнения (execution engine). Он превращает байткод в реальные действия процессора. Здесь важно не путать два режима.
- Интерпретация — JVM читает и выполняет байткод инструкция за инструкцией. Старт быстрый, но сам код медленнее нативного.
- JIT-компиляция (Just-In-Time) — «горячие» участки, которые вызываются часто, компилируются в машинный код прямо во время работы и дальше исполняются нативно.
HotSpot называется так именно потому, что ищет «горячие точки» и компилирует их. В нем работает многоуровневая компиляция (tiered compilation): быстрый компилятор C1 дает раннюю оптимизацию, а более агрессивный C2 перекомпилирует самые нагруженные методы. За счет профиля исполнения JIT применяет оптимизации, недоступные обычному компилятору «наперед»: инлайнинг, устранение проверок, спекулятивные допущения с откатом (деоптимизацией), если допущение не подтвердилось.
Это упрощенная модель: помимо интерпретатора и JIT существует и заранее-компиляция (AOT), но для стандартного HotSpot базовый путь — именно «интерпретация плюс JIT».
Сборка мусора: управление памятью
Третья подсистема — сборщик мусора (garbage collector, GC). В Java память под объекты выделяется в куче при new, а освобождается автоматически: GC находит объекты, до которых из программы больше нельзя добраться по ссылкам, и возвращает их память. Ручного free, как в C или C++, нет, и это снимает целый класс ошибок вроде двойного освобождения.
«Мусор» здесь — строгий термин: это недостижимые объекты, а не просто давно не использованные. Достижимость считается от корней (GC roots): локальных переменных на стеке, статических полей и подобного.
В современных сборках GC не один, их выбирают под задачу:
- G1 (Garbage-First) — сборщик по умолчанию для серверных конфигураций начиная с Java 9, а с Java 27 — во всех окружениях (прежде на слабых машинах с одним ядром или малой памятью JVM выбирала Serial); делит кучу на регионы и балансирует паузы и пропускную способность;
- ZGC и Shenandoah — сборщики с очень короткими, субмиллисекундными паузами, рассчитанные на большие кучи и низкую задержку; в актуальных JDK ZGC работает только в поколенческом (generational) режиме;
- Parallel GC — ориентирован на максимальную пропускную способность, паузы длиннее.
Выбор GC задается флагом JVM (например, -XX:+UseZGC). Универсально «лучшего» сборщика нет: G1 — разумный дефолт для большинства приложений, ZGC и Shenandoah берут там, где критична задержка, Parallel — где важнее общая пропускная способность батч-обработки.
Платформонезависимость и другие языки на JVM
Принцип write once, run anywhere («написано однажды — запускается везде») означает, что переносим именно байткод, а не исходник. Абстракцию от железа обеспечивает связка «единый формат .class плюс реализация JVM под каждую ОС». Полностью абсолютной переносимости это не гарантирует: поведение может отличаться из-за версии Java, доступной памяти или обращения к платформенным API, поэтому сборку под несколько сред все равно проверяют.
Побочный, но важный эффект: JVM исполняет любой корректный байткод, а не только полученный из Java. Поэтому на ней работают и другие языки — Kotlin, Scala, Groovy, Clojure: их компиляторы тоже выдают .class-файлы. Для программиста это значит, что экосистема JVM (библиотеки, инструменты, сборщики мусора) доступна из нескольких языков сразу.
Выводы
- JVM исполняет промежуточный байткод из
.class-файлов, а не машинный код конкретного процессора; переносимость обеспечивает именно единый формат байткода плюс реализация JVM под каждую ОС. - JVM, JRE и JDK — разные уровни: машина, среда выполнения с библиотеками и комплект разработчика с инструментами.
- Три подсистемы JVM: загрузчик классов (load-link-init с проверкой байткода), механизм выполнения (интерпретация плюс JIT в HotSpot) и сборщик мусора.
- Память в куче освобождается автоматически: GC собирает недостижимые объекты; по умолчанию используется G1, для низких задержек берут ZGC или Shenandoah.
- На JVM компилируются и другие языки (Kotlin, Scala, Groovy), потому что машине важен байткод, а не исходный язык.
Где применяется / связь с практикой
Понимание JVM — это база для отладки и настройки Java-приложений: выбор и тюнинг сборщика мусора, чтение стектрейсов и .class-файлов, поиск утечек памяти и «прогрев» JIT под нагрузкой. Без этой модели производительность и поведение приложения в проде остаются черным ящиком.
Освойте тему на практике
Разобрать устройство JVM на практике — от компиляции первого класса до профилирования GC — удобно на курсе Java Basic: там byte-code, память и инструменты JDK показывают на живых примерах. Посмотреть формат и уровень занятий можно на открытых уроках Otus — это бесплатно и помогает понять, подходит ли вам направление.
FAQ
Java — компилируемый или интерпретируемый язык?
И то и другое. Исходник компилируется в байткод компилятором javac, а байткод во время работы и интерпретируется, и частично компилируется в машинный код JIT-компилятором внутри JVM.
Можно ли запустить программу, имея только JRE, без JDK?
Да. JRE (JVM плюс библиотеки) достаточно, чтобы исполнять готовый байткод. JDK нужен, чтобы компилировать исходники и пользоваться инструментами вроде javac и javap. С Java 11 среду выполнения обычно берут внутри JDK или собирают через jlink.
Нужно ли вручную освобождать память в Java?
Нет, память освобождает сборщик мусора автоматически. Задача разработчика — не удерживать лишние ссылки на ненужные объекты, иначе они останутся достижимыми и не будут собраны, что и приводит к утечкам памяти.



