JDK, JRE и JVM: как устроена Java-платформа

Введение

Когда начинающий разработчик впервые сталкивается с Java, он почти сразу встречает аббревиатуры JDK, JRE и JVM. На первый взгляд они кажутся похожими, поэтому важно разобраться, для чего они нужны и как связаны между собой.
Коротко связь между этими понятиями можно описать так:
  • JDK — комплект разработки Java. Он содержит инструменты для создания, компиляции, запуска и отладки Java-программ
  • JRE — среда выполнения Java. Она предоставляет компоненты, необходимые для запуска Java-приложений
  • JVM — виртуальная машина Java. Она выполняет байт-код программы и управляет ее работой во время выполнения
Проще говоря, разработчик обычно устанавливает JDK, пишет код на Java и компилирует его в байт-код. Чтобы запустить программу, используются runtime-компоненты: JVM, стандартные библиотеки и служебные файлы. Раньше эту среду выполнения называли JRE, а внутри JDK был отдельный каталог jre. Начиная с Java 9 структура изменилась: runtime-компоненты входят в установленную сборку JDK, а JVM загружает классы и выполняет байт-код.
Рис. 1. Связь JDK, JRE и JVM в классической модели

Что такое JDK

JDK (Java Development Kit) — это комплект разработки Java. Он содержит все основное, что требуется для создания, компиляции и запуска Java-программ: компилятор, инструменты командной строки, стандартные библиотеки и компоненты среды выполнения.
Если по-простому, то JDK — это программный пакет, который вы скачиваете и устанавливаете, чтобы создавать, компилировать и запускать приложения на Java.
JDK поставляется в виде готовых сборок от разных поставщиков. Например, можно установить Oracle JDK, Eclipse Temurin, Amazon Corretto, Azul Zulu, Microsoft Build of OpenJDK и другие сборки.

Что входит в JDK

В состав JDK входят:
  • Компилятор javac. Он преобразует исходные файлы .java в файлы .class, содержащие байт-код
  • Команда java. Она запускает Java-приложения
  • Стандартные библиотеки Java. Это готовые классы и API, которые используются в программах: коллекции, строки, ввод-вывод, работа с сетью, датой и временем и многое другое
  • Инструменты разработки и диагностики. Например, jar, javadoc, jshell, jdb и другие команды
  • Компоненты среды выполнения. Они необходимы для запуска Java-программ, поэтому отдельная установка JRE для разработки обычно не требуется

JDK и компилятор Java

Один из важнейших инструментов JDK — компилятор javac.
Java-программа начинается с исходного файла .java. Это обычный текстовый файл, в котором написан код на языке Java. Компилятор преобразует такой файл в .class-файл, содержащий байт-код.
Байт-код — это не машинный код конкретного процессора и не обычный исполняемый файл вроде .exe. Это промежуточный формат, который понимает JVM. Благодаря этому один и тот же скомпилированный Java-код может выполняться на разных операционных системах, если для них есть подходящая JVM.
Упрощенная цепочка выглядит так:
Исходный код .java → компиляция javac → байт-код .class → выполнение в JVM
Версии JDK
При установке JDK требуется выбрать версию Java. Версия JDK определяет, какие возможности языка доступны при компиляции.
Например, в современных версиях Java switch можно использовать не только как оператор, но и как выражение, которое возвращает значение. Для такой записи требуется JDK 14 или более новая версия. Более старый компилятор javac такой код не примет и сообщит о синтаксической ошибке.
Java сохраняет высокую степень обратной совместимости, поэтому для учебных целей можно установить последнюю версию JDK. В реальных проектах часто выбирают LTS-версии, поскольку они рассчитаны на длительную поддержку и считаются более стабильным выбором для промышленной разработки.
Java SE, Jakarta EE и разные сборки JDK
Раньше при работе с Java часто говорили о разных редакциях платформы:
  • Java SE (Java Standard Edition) — стандартная платформа Java. Именно с нее начинают большинство разработчиков
  • Java EE (Java Enterprise Edition) — набор технологий для корпоративной разработки. Сейчас эти технологии развиваются в проекте Jakarta EE
  • Java ME (Java Micro Edition) — редакция для ограниченных устройств. Сегодня для начинающего Java-разработчика она обычно неактуальна.
Для обычной разработки чаще всего устанавливают JDK для Java SE: Oracle JDK, Eclipse Temurin, Amazon Corretto, Azul Zulu, Microsoft Build of OpenJDK или другую готовую сборку.
Начинающему разработчику достаточно установить одну из современных сборок JDK для Java SE. Подробности установки обычно сводятся к двум действиям: установить JDK и корректно настроить системные переменные, например JAVA_HOME и PATH. Подробности этого процесса описаны в статье «Установка и настройка Java».
JDK в командной строке
Установка JDK добавляет команды java, javac и прочие в вашу командную строку, с помощью которой можно компилировать и запускать исходный код на Java. Подробности читайте в статье «Компиляция и запуск Java-программ».
Простая программа на Java
Традиционно первая программа на Java состоит из простого кода, выводящего на экран простые фразы. Подробнее подобная программа разбирается в статье «Java с нуля: первая программа».
Что такое JRE
JRE (Java Runtime Environment) — это среда выполнения Java, то есть набор компонентов, необходимых для запуска Java-программ.
JRE включает:
  • JVM — виртуальную машину Java, которая отвечает не только за выполнение байт-кода, но и за загрузку классов, управление памятью, сборку мусора и другие процессы, необходимые во время работы программы
  • стандартные библиотеки Java — готовые классы и API, которые использует программа
  • служебные компоненты среды выполнения — файлы и механизмы, необходимые для запуска и работы приложения
Проще говоря, JRE предоставляет среду, в которой Java-программа может запуститься и работать. Без такой среды файл с байт-кодом сам по себе не выполняется операционной системой напрямую.
В следующих разделах разберем, как эти компоненты связаны между собой.
Что такое среда выполнения?
Для запуска программы недостаточно одного файла с кодом. Программе требуется среда выполнения — набор компонентов, которые помогают загрузить код, выделить память, подключить библиотеки и обеспечить взаимодействие с операционной системой.
обычных нативных приложениях значительную часть этой работы берет на себя операционная система: она запускает процесс, выделяет память, предоставляет доступ к файлам, сети и другим системным ресурсам. Но такие программы обычно зависят от конкретной ОС и аппаратной платформы, под которую они были скомпилированы.
Java устроена иначе. Исходный код Java компилируется не напрямую в машинный код конкретного процессора, а в байт-код — промежуточный формат, который выполняет JVM. Благодаря этому одна и та же Java-программа может запускаться на разных операционных системах, если для них установлена подходящая среда выполнения Java.
Среда выполнения Java
Программное обеспечение можно представить как набор слоев, расположенных поверх оборудования компьютера. Каждый слой предоставляет службы, которыми пользуются слои выше. Операционная система управляет оборудованием и системными ресурсами, а среда выполнения Java работает поверх операционной системы и предоставляет компоненты, необходимые именно Java-программам.
JRE помогает сгладить различия между операционными системами, позволяя Java-программам запускаться на разных платформах без изменения исходного кода. При этом сама программа выполняется не напрямую операционной системой, а через JVM.
Одна из важных возможностей JVM — автоматическое управление памятью. Благодаря этому разработчику не приходится вручную выделять и освобождать память: JVM сама следит за объектами, которые больше не используются, и удаляет их с помощью сборщика мусора.
Если коротко, JRE можно представить как промежуточный слой между Java-программой и операционной системой. Этот слой скрывает часть различий между платформами и предоставляет единообразную среду для запуска Java-приложений.
Как JRE работает с JVM
JRE предоставляет среду, в которой запускается Java-программа. Она включает JVM, стандартные библиотеки Java и служебные компоненты, необходимые во время выполнения.
Когда пользователь запускает Java-приложение, например командой java, среда выполнения передает управление JVM. JVM загружает необходимые классы, проверяет байт-код, подключает стандартные библиотеки и начинает выполнение программы.
При этом JVM отвечает не только за запуск байт-кода. Во время работы программы она управляет памятью, выполняет сборку мусора, следит за потоками и применяет JIT-компиляцию, чтобы часто выполняемые участки кода работали быстрее.
Проще говоря, JRE предоставляет все необходимое окружение для запуска Java-программы, а JVM внутри этой среды непосредственно выполняет байт-код.
Установка и использование JRE
Несмотря на концептуальную сторону JRE, в реальной практике — это просто программное обеспечение, установленное на компьютере для запуска Java-программ. Как разработчик вы будете работать с JDK и JVM, т.к. эти компоненты необходимы для разработки и запуска ваших приложений. Как пользователь вы будете использовать JRE для их запуска.
Обычному пользователю отдельная установка JRE требуется не всегда. Многие современные приложения поставляются вместе со своей средой выполнения, поэтому Java не приходится устанавливать отдельно. Если же конкретная программа просит установить Java, следует ориентироваться на требования этой программы: она может требовать определенную версию Java.
Узнать установленную версию Java можно в командной строке: java -version
Проверить, откуда запускается команда java, можно так:

Для Linux и macOS:

which java

Для Windows:

where java

Для PowerShell:

Get-Command java
Среда выполнения Java в эксплуатации
На этапе разработки среда выполнения Java обычно остается почти незаметной: разработчик пишет код, запускает приложение из IDE или командной строки и редко задумывается о том, какие runtime-компоненты участвуют в этом процессе.
В эксплуатации роль среды выполнения становится заметнее. Для стабильной работы Java-приложения важно правильно выбрать версию Java, настроить параметры JVM, контролировать использование памяти, следить за сборкой мусора, потоками и другими показателями работы приложения.
Особенно это важно для серверных и облачных приложений, где Java-программа часто запускается в контейнере с ограниченными ресурсами. Например, для приложения можно задавать параметры памяти JVM, такие как максимальный размер heap-памяти, и затем отслеживать, как приложение использует выделенные ресурсы.
Для диагностики и мониторинга обычно используют параметры JVM, инструменты JDK и внешние системы наблюдаемости: логи, метрики, профилировщики, APM-сервисы и мониторинг контейнеров.
JRE в devops
DevOps — это подход, который объединяет разработку, эксплуатацию и автоматизацию процессов поставки программного обеспечения. В Java-проектах DevOps-инженеру или системному администратору важно понимать, как запускается Java-приложение, какая версия Java используется, какие параметры JVM заданы и как приложение потребляет память и другие ресурсы.
Иначе говоря, в эксплуатации управляют не столько «JRE» как отдельной сущностью, сколько средой запуска Java-приложения: версией Java, JVM-параметрами, контейнером, системными ресурсами и средствами мониторинга.
Что такое JVM
JVM (Java Virtual Machine) — это виртуальная машина Java. Она запускает Java-программы, выполняя байт-код — промежуточный формат, в который компилятор преобразует исходный код .java.
Главная идея JVM в том, что Java-программа не привязана напрямую к конкретной операционной системе. Разработчик компилирует исходный код в байт-код, а затем этот байт-код выполняется JVM. Если для операционной системы установлена подходящая JVM, программа может запускаться без изменения исходного кода.
Для чего используется JVM
У JVM есть несколько важных задач:
  • Выполнение байт-кода. JVM загружает скомпилированные классы, проверяет байт-код и выполняет программу
  • Переносимость Java-программ. Один и тот же байт-код может выполняться на разных операционных системах, если для них есть подходящая JVM. С этим связан принцип Java: «написал один раз — запускай где угодно»
  • Управление памятью. JVM выделяет память для объектов, следит за их использованием и освобождает память от объектов, которые больше не нужны программе
  • Оптимизация выполнения. Современные JVM не только интерпретируют байт-код, но и используют JIT-компиляцию: часто выполняемые участки кода могут преобразовываться в машинный код во время работы программы
Когда Java появилась в 1995 году, такая модель была важным преимуществом. Многие программы тогда приходилось разрабатывать и собирать отдельно под конкретную операционную систему и аппаратную платформу, а в языках вроде C и C++ разработчик часто сам отвечал за управление памятью. JVM помогла решить обе задачи: повысила переносимость программ и взяла на себя значительную часть работы с памятью.
JVM можно рассматривать с двух сторон: как спецификацию и как программу, которая запускает Java-приложения.
  • Техническое определение: JVM — это спецификация виртуальной машины, которая определяет, как должен выполняться байт-код Java-программ и какие требования должна соблюдать конкретная реализация JVM
  • Практическое определение: JVM — это программа, которая запускает Java-приложение, выполняет его байт-код и управляет ресурсами во время работы: памятью, потоками, загрузкой классов и сборкой мусора
Сборка мусора
В языках вроде C и C++ разработчик часто сам отвечает за выделение и освобождение памяти. В Java большую часть этой работы берет на себя JVM. Она управляет памятью программы и освобождает ее от объектов, которые больше не используются.
Этот процесс называется сборкой мусора. Во время работы программы JVM отслеживает объекты, на которые больше нет ссылок, и удаляет их из памяти. Благодаря этому разработчику не приходится вручную освобождать память после использования объектов.
В первые годы Java часто критиковали за производительность. Java-программы выполнялись не напрямую на процессоре, как нативный код C или C++, а через JVM. Кроме того, сборка мусора могла создавать дополнительные паузы во время работы приложения.
Со временем JVM стала значительно эффективнее. В современных реализациях используются разные алгоритмы сборки мусора, JIT-компиляция и другие оптимизации, которые позволяют Java-приложениям показывать высокую производительность во многих сценариях.
Память в JVM
Памятью Java-программы управляет JVM. Она выделяет память для объектов, хранит данные во время выполнения программы и освобождает память от объектов, которые больше не используются.
Упрощенно память JVM можно разделить на несколько важных областей:
  • Metaspace, или метапространство — область памяти, в которой JVM хранит метаданные загруженных классов: информацию о классах, методах, полях и другие служебные данные.
  • Heap, или куча — область памяти, в которой хранятся объекты, созданные во время работы программы. Например, объект, созданный с помощью new, обычно размещается в куче.
  • Stack, или стек — область памяти, связанная с выполнением методов. В стеке хранятся вызовы методов, локальные переменные примитивных типов и ссылки на объекты, которые находятся в куче.
Три значения термина JVM
Термин JVM используют в трех связанных, но разных смыслах:
  • Спецификация JVM — официальный документ, который описывает, как должна работать виртуальная машина Java
  • Реализация JVM — конкретная программа, созданная по этой спецификации, например HotSpot JVM или Eclipse OpenJ9
  • Экземпляр JVM — запущенный процесс виртуальной машины, который выполняет конкретное Java-приложение
Разберем каждый из этих смыслов подробнее.
1. Спецификация JVM
Спецификация JVM описывает требования к виртуальной машине Java. Она определяет формат .class-файлов, набор инструкций байт-кода, правила загрузки и проверки классов, модель памяти, работу со стеком, исключениями, потоками и другими механизмами выполнения программы.
При этом спецификация не диктует, как именно должна быть устроена JVM внутри. Разработчики конкретной реализации могут сами выбирать внутренние алгоритмы, способы оптимизации, устройство сборщика мусора и другие детали. Главное — чтобы реализация корректно выполняла байт-код и соответствовала требованиям спецификации.
Именно благодаря такому подходу могут существовать разные реализации JVM. Они отличаются внутренним устройством и производительностью, но способны запускать один и тот же байт-код Java-программы.
2. Реализация JVM
Спецификация JVM описывает, как должна работать виртуальная машина Java, но сама по себе не запускает программы. Для выполнения Java-приложений требуется конкретная реализация JVM.
Самая известная реализация JVM — HotSpot JVM. Она входит в состав OpenJDK и большинства популярных сборок JDK на его основе: Oracle JDK, Eclipse Temurin, Amazon Corretto, Azul Zulu, Microsoft Build of OpenJDK и других. Исходный код HotSpot находится в репозитории JDK проекта OpenJDK.
При этом HotSpot — не единственная реализация JVM. Существуют и другие реализации, например Eclipse OpenJ9. Они тоже должны соответствовать спецификации JVM, но могут отличаться внутренним устройством, производительностью, настройками, алгоритмами сборки мусора и диагностическими возможностями.
На практике разработчик обычно не устанавливает JVM отдельно. Он выбирает и устанавливает готовую сборку JDK, а JVM уже входит в ее состав. Для начинающего разработчика этого достаточно: установленный JDK содержит и инструменты разработки, и виртуальную машину для запуска Java-программ.
3. Экземпляр JVM
После того как реализация JVM создана и входит в состав JDK, ее можно использовать для запуска Java-программ. Когда вы запускаете приложение командой java, операционная система запускает процесс JVM. Этот запущенный процесс и называют экземпляром JVM.
Обычно, когда разработчики говорят «JVM», они имеют в виду именно работающий экземпляр JVM: процесс, который выполняет Java-приложение, использует память, загружает классы, управляет потоками и выполняет сборку мусора.
Например, фраза:

Сколько памяти использует JVM на этом сервере?

обычно означает:

Сколько памяти использует запущенный процесс Java-приложения?

А если программа завершилась из-за ошибки, например из-за переполнения стека, говорят, что ошибка произошла в работающей JVM. Формально при этом «ломается» не JVM как технология, а завершается конкретный процесс, в котором выполнялась программа.
Загрузка и выполнение .class-файлов в JVM
Мы уже говорили, что JVM запускает Java-приложения. Но перед тем как выполнить программу, JVM должна найти, загрузить и проверить скомпилированные классы.
Java-приложение состоит из классов и других связанных с ними элементов: интерфейсов, enum, аннотаций и т. д. После компиляции они представлены в виде .class-файлов или упакованы в JAR-архивы. Чтобы выполнить программу, JVM загружает необходимые классы в память и подготавливает их к работе.
Загрузчик классов в JVM
Загрузчик классов — это часть механизма JVM, отвечающая за загрузку классов во время выполнения программы. Он находит нужные .class-файлы, загружает их в память и делает доступными для дальнейшего выполнения.
Классы обычно загружаются не все сразу, а по мере необходимости. Такой подход называют ленивой загрузкой. Например, если какая-то часть программы ни разу не была использована, связанные с ней классы могут так и не загрузиться во время работы приложения.
JVM также использует уже загруженные классы повторно, чтобы не загружать один и тот же класс заново без необходимости.
Любая реализация JVM содержит механизм загрузки классов. Спецификация JVM описывает общие правила загрузки, связывания и инициализации классов, а конкретная реализация JVM отвечает за то, как именно эти процессы устроены внутри.
Механизм выполнения в JVM
После загрузки и проверки классов JVM начинает выполнять байт-код программы. Выполнение обычно начинается с точки входа, например с метода main, а затем продолжается по мере вызова других методов и загрузки дополнительных классов.
За выполнение байт-кода отвечает механизм выполнения JVM. Он интерпретирует инструкции байт-кода, а часто выполняемые участки кода может компилировать в машинный код с помощью JIT-компилятора. Благодаря этому современные JVM не только запускают Java-программы, но и оптимизируют их работу во время выполнения.
Во время работы программа может обращаться к файлам, сети, памяти, потокам и другим системным ресурсам. JVM выступает промежуточным слоем между Java-программой и операционной системой: она выполняет байт-код, управляет памятью, контролирует работу потоков и обращается к возможностям ОС, когда программе требуются внешние ресурсы.
Управление системными ресурсами
Во время работы Java-программа использует разные ресурсы: память, файлы, сеть, потоки выполнения и другие возможности операционной системы. JVM и стандартные библиотеки Java создают промежуточный слой между программой и ОС, благодаря которому один и тот же Java-код может работать на разных платформах.
Особое место занимает память. JVM выделяет память для объектов, хранит ссылки на них и освобождает память от объектов, которые больше не используются. За очистку неиспользуемых объектов отвечает сборщик мусора.
Например, когда в программе создается объект с помощью new, память для него выделяется в куче JVM. Разработчику не приходится вручную запрашивать память у операционной системы и затем освобождать ее: этим занимается JVM.
С файлами, сетью и другими внешними ресурсами ситуация немного другая. Java-программа обращается к ним через стандартные API, например через классы для работы с файлами или сетевыми соединениями. Эти API скрывают часть различий между операционными системами, а JVM и стандартные библиотеки взаимодействуют с ОС и передают ей необходимые запросы.
Поэтому JVM не только выполняет байт-код, но и помогает Java-программе работать в конкретной операционной системе: управляет памятью, поддерживает работу потоков и обеспечивает связь между Java-кодом, стандартными библиотеками и системными возможностями ОС.
Эволюция JVM: прошлое, настоящее и будущее
С появлением Java и JVM широкое распространение получили две важные идеи: переносимость программ между платформами и автоматическое управление памятью.
Переносимость выразилась в принципе «написал один раз — запускай где угодно». Разработчик компилирует Java-код в байт-код, а затем этот байт-код может выполняться на разных операционных системах, если для них есть подходящая JVM.
Автоматическое управление памятью также стало одним из важных преимуществ Java. В языках вроде C и C++ разработчику часто приходится самостоятельно выделять и освобождать память. В Java эту работу во многом берет на себя JVM: она управляет памятью и очищает ее от объектов, которые больше не используются.
Изначально JVM создавалась прежде всего для Java, но со временем стала платформой и для других языков. Сегодня на JVM работают Scala, Kotlin, Groovy, Clojure и другие языки. Они используют возможности JVM: выполнение байт-кода, управление памятью, сборку мусора, JIT-компиляцию и развитую экосистему библиотек.
JVM продолжает развиваться и остается важной частью Java-платформы. Ее роль не ограничивается запуском Java-кода: она обеспечивает переносимость, производительность и единый runtime для разных языков и приложений.
Оцените статью, если она вам понравилась!