- Что такое Java Virtual Machine
- История развития JVM
- Для чего нужна JVMjava
- Сборка мусора
- Преимущества Java Virtual Machine
- Недостатки
- Кто разрабатывает JVM
- Компоненты Java Virtual Machine
- Спецификация
- Реализация
- Экземпляр
- Загрузка и выполнение class-файлов в JVM
- Загрузчик классов
- Механизм выполнения
- Управление системными ресурсами
- Аналоги Java Virtual Machine
- Перспективы развития JVM
Что такое Java Virtual Machine
JVM, или Java Virtual Machine — это программа, которая позволяет запускать и выполнять другие программы, написанные на языке программирования Java, хотя иногда используется и для запуска программ на других языках. Это ключевой элемент в экосистеме Java — он обеспечивает запуск и выполнение программ независимо от операционной системы и устройства, так как реализации JVM следуют принципу WORA (write once, run anywhere) — «напиши один раз, запусти где угодно».
История развития JVM
История развития Java Virtual Machine тесно связана с историей создания Java. И для начала следует сказать о предпосылках появления этого языка и виртуальной машины. Во-первых, раньше разработчикам приходилось учитывать, на какой операционной системе будет запускаться их приложение, так как программа, которую они написали под одну ОС, могла не запуститься на другой ОС и тем более вряд ли исполнялась бы корректно. Поэтому для каждой ОС нужно было писать отдельную программу, что значительно осложняло (в том числе из-за необходимости устраивать больше тестов) и замедляло разработку программ. Во-вторых, возникали определенные требования к безопасности.
В 1991 году в компании Sun Microsystems началась работа над проектом «Green», целью которого являлось создание языка программирования для бытовой электроники, например, для телевизоров. Команду инженеров возглавлял Джеймс Гослинг. Изначально новый язык получил название Oak, но позже его изменили на Java. В 1995 году были официально представлены и язык, и его среда выполнения, включая виртуальную машину. Проблема кроссплатформенности была решена. Программа, написанная на Java, сначала компилируется в байт-код (низкоуровневое представление программы, которое выполняется виртуальной машиной), а затем интерпретируется и исполняется виртуальной машиной, что позволяет запускать ее везде, где установлена JVM.
С того момента в JVM вносились различные изменения, например, были улучшены механизмы управления памятью, а в 1997 году добавлена технология JIT. В 2010 году компания Oracle приобрела Sun Microsystems и продолжает развивать JVM по сей день.
Для чего нужна JVM
Основное назначение JVM — предоставление среды выполнения для программ, а точнее, непосредственно запуск и выполнение байт-кода. Но есть и другие функции и особенности, которые придают JVM тот вид, который она сейчас имеет:
- автоматическое управление памятью, которое представляет из себя выделение необходимой памяти для объектов и освобождение памяти, занятой объектами, больше не использующимися программой, за счет сборки мусора;
- обеспечение работы на любой платформе, следование принципу WORA;
- обеспечение безопасности — реализуется путем выполнения программы в изолированной среде и верификации байт-кода (проверки на наличие некорректных или опасных частей программы).
Все это значительно упрощает разработку: программисту не нужно вручную управлять памятью, писать отдельную программу для каждой ОС и беспокоиться о возможной небезопасности программы.
Сборка мусора
Сборка мусора, или Garbage Collection, — часть процесса управления памятью, которая заключается в удалении объектов, которые больше не используется программой, и, соответственно, в восстановлении памяти, которую они занимали. Важно сказать, что сборка мусора — это автоматический процесс, позволяющий разработчикам снять с себя обязанности по удалению ненужных объектов и избежать утечек памяти. Утечка памяти — это проблема, часто возникающая в программах, написанных на C и C++. Утечка памяти возникает, когда неиспользуемые объекты не удаляются, что ведет к неконтролируемому сокращению объема свободной памяти. В какой-то момент память полностью перестанет выделяться, тогда программа в лучшем случае завершится, а в худшем — повредятся важные данные или файлы.
О том, как конкретно должна быть реализована сборка мусора, в спецификациях не сказано, но все же можно выделить основные функции работы сборщиков: инициализацию, маркировку, удаление, компактизацию — выполняется не всегда.
Рассмотрим этапы работы сборщиков мусора более подробно:
- Инициализация — своеобразная проверка состояния памяти, определение надобности в очистке.
- Идентификация живых объектов, или маркировка, — сборщик находит и помечает все объекты, которые могут быть достигнуты из корней. Несколько примеров корней: активные потоки, локальные переменные в стеке, специальные объекты Java VM и другие.
- Удаление мертвых объектов — сборщик освобождает все фрагменты памяти, которые не были промаркированы на предыдущем этапе.
- Компактизация — это уплотнение памяти с целью уменьшения ее фрагментации. То есть, при компактизации оставшиеся объекты располагаются в начале кучи непрерывно.
Также существует сборка мусора по поколениям. В таком случае куча — область памяти, где хранятся все объекты и связанные с ними данные, делится на поколения:
- молодое поколение (young generation) — сюда попадают новые объекты. Это поколение делится на две части: Eden Space (новые объекты) и Survivor Spaces (объекты, не удаленные после одного цикла сборки мусора);
- старое поколение (old generation) — сюда объекты попадают из молодого поколения, если они не были удалены после нескольких циклов сборки мусора;
- постоянное поколение (permgen) до Java 8 и Metaspace после — здесь хранятся метаданные загруженных классов. Основное отличие Metaspace от PermGen — эта область кучи динамически изменяется при необходимости.
Виды сборщиков мусора:
- Serial Garbage Collector — базовый сборщик мусора, эффективный для небольших приложений. Он работает в однопоточном режиме, а значит, в момент сборки мусора все другие потоки приложения останавливаются. Соответственно, этот инструмент неэффективен для многопоточных приложений, так как его работа вызовет большие паузы. Чем больше паузы, тем хуже отзывчивость приложения.
- Parallel Garbage Collector — этот сборщик выполняет свои задачи в многопоточном режиме и имеет более высокую производительность по сравнению с Serial GC, но тоже останавливает все потоки приложения на время своей работы. Данный инструмент приемлем для многопоточных приложений, если паузы в их работе не критичны.
- CMS (Concurrent Mark-Sweep) Garbage Collector — этот сборщик работает параллельно с потоками приложения, тем самым уменьшая длительность пауз на сборку мусора. Фазы его работы: маркировка корневых объектов, обход объектов, перемаркировка — отслеживание изменений, которые произошли в куче с начала прошлой фазы, удаление немаркированных объектов. Во время второй и четвертой фазы работа приложения не приостанавливается.
- G1 (Garbage-First) Garbage Collector — более современный сборщик, который делит кучу на регионы и сначала очищает память в наиболее заполненных регионах. Его работа тоже подразумевает паузы, но их длительность предсказуема, и они возникают лишь на некоторых этапах работы сборщика: две короткие паузы во время маркировки корневых объектов и завершения процесса маркировки, пауза во время сборки мусора, которая тоже обычно короче, чем у других GC. Во время обхода объектов, доступных из корневого объекта, потоки приложения не останавливаются. Данный сборщик может быть сложным в настройке.
- Z Garbage Collector (ZGC) — это сборщик, подходящий для больших и сложных приложений, для которых критичны большие паузы и низкая производительность. Он может управлять большими размерами памяти — до нескольких терабайт, уплотняет память и обеспечивает минимальные паузы — до 10 миллисекунд. Такие короткие паузы обусловлены тем, что большую часть работы ZGC выполняет, не прерывая работу приложения.
Важно также отметить, что многие параметры сборщиков мусора могут быть настроены под конкретные нужды приложения, например, можно установить максимальный размер кучи.
Преимущества Java Virtual Machine
- Кроссплатформенность. Программы, которые были написаны на языке программирования Java, компилируются в байт-код. Байт-код может быть запущен и выполнен на любой платформе, на которую можно установить JVM. Так реализуется кроссплатформенность.
- Безопасность. JVM изолирует отдельные программы, написанные на Java, друг от друга, а также не позволяет им напрямую взаимодействовать с ресурсами операционной системы. При необходимости параметры безопасности могут быть настроены дополнительно.
- Управление памятью. JVM автоматически управляет памятью: выделяет необходимый объем для объектов, отслеживает ее использование, при наличии ненужных объектов освобождает память, предотвращает ее утечки. Есть и обратная сторона: такое поведение может негативно влиять на производительность.
- Проверка байт-кода. Перед выполнением программы байт-код проходит верификацию, которая позволяет определить, корректно ли была написана и скомпилирована программа на Java. Верификация — это залог эффективного и безопасного исполнения байт-кода.
- Дополнительные технологии, созданные для облегчения разработки и ускорения выполнения программ. Например, Just-in-time (JIT) компиляция — байт-код компилируется в машинный код во время выполнения программы, что повышает производительность.
- Поддержка других языков программирования. Например, некоторые из них изначально создавались для JVM. Даже существует такое понятие, как JVM-язык — Groovy, Scala, Closure, Ceylon, Kotlin и другие.
Недостатки
- Производительность и задержки при запуске. Это напрямую связано с принципом работы виртуальной машины: ее движок выполняет байт-код, а не непосредственно машинный код. Частично эта проблема решается JIT-компиляцией, но JIT-компиляция может вызывать задержки при первом запуске программы. Также задержки возникают из-за необходимости загрузки классов. Иногда это не критично, но, например, при работе с приложениями, которые часто перезапускаются, нужно учитывать эту особенность. На производительность оказывает влияние и сборка мусора.
- Высокое потребление ресурсов: оперативной памяти и CPU time. Программы, написанные на Java, требуют довольно большого объема памяти для запуска и выполнения, а, например, сборка мусора потребляет процессорные ресурсы.
- Трудности с отладкой. Из-за многослойной архитектуры процесс поиска и исправления ошибок может быть затруднен, так как разработчик не всегда понимает, где именно возникает ошибка. Для решения этой проблемы существуют специальные инструменты, которые позволяют отслеживать выполнение программы и находить ошибки.
- Несмотря на то, что все реализации JVM создаются в соответствии со стандартами и проходят тесты, риск возникновения поломок в самой виртуальной машине все же присутствует, хотя и очень низкий. Здесь же можно упомянуть, что не исключены проблемы с безопасностью.
Кто разрабатывает JVM
Основным разработчиком JVM сейчас является компания Oracle. Именно эта компания выпускает официальную реализацию Java Development Kit (JDK) — набор инструментов для разработки на Java, который включает в себя компилятор, виртуальную машину, стандартную библиотеку и документацию. Все эти компоненты регулярно обновляются и улучшаются. Существует проект OpenJDK — это полностью открытая реализация JDK, разработкой которой занимается как компания Oracle, так и сообщество других разработчиков и компаний.
Некоторые компании занимаются разработкой собственных JVM. Эти проекты могут быть открытыми, как OpenJDK, а могут быть закрытыми и платными. Примером открытого проекта служит OpenJ9 от компании IBM, а закрытого — Zing от Azul. Конечно, некоторые виртуальные машины платные не просто так — в них внедрены уникальные технологии, которых нет в бесплатных версиях. Часто такие технологии направлены на повышение производительности. Также в теории даже один разработчик, если он обладает достаточными знаниями и навыками, может взять за основу открытый проект и разработать собственную виртуальную машину.
Важно упомянуть и такие вещи, как JSR — это предложения по улучшению или введению новых функций для JVM, Java и JCP. Это союз компаний, сообществ и отдельных программистов, который рассматривает JSR. Таким образом решения принимаются согласованно.
Каждый, кто хочет быть в курсе новостей и участвовать в дискуссиях, может подписаться на мейлинг-лист. Мейлинг-листов существует много — узкие (по разным темам и компонентам) и широкие (по целым проектам и программам).
В итоге можно сказать, что разработка JVM — это процесс, который реализуется совместными усилиями как крупных организаций, так и независимых специалистов.
Компоненты Java Virtual Machine
JVM состоит из трех основных компонентов: спецификации, реализации и экземпляра. Понимание специфики этих трех компонентов позволяет глубже понять, как устроена JVM.
Спецификация
Спецификация (specification) — это свод описаний и требований того, что должна делать JVM: интерпретировать байт-код, управлять памятью и так далее. Одним словом, виртуальная машина должна правильно запускать и выполнять программы. При этом деталей реализации такого поведения спецификация не предоставляет. Например, там не сказано, какой конкретно алгоритм сбора мусора должен использоваться, поэтому разработчики могут создавать уникальные реализации, каждая из которых будет иметь свои преимущества. Главное, чтобы требования спецификации выполнялись. Спецификация разделена на главы, каждая из которых описывает определенную часть работы JVM. Например, первая глава — вводная, а вторая описывает архитектуру.
Роль спецификации заключается в создании стандарта для всех Java VM. Это нужно для того, чтобы гарантировать совместимость программ с виртуальными машинами, в том числе обеспечить обратную совместимость.
Обновления и изменения спецификации регулярно осуществляет JCP — последнее из них было выпущено в 2024 году.
Реализация
Реализация — это конкретная программа, написанная в соответствии со спецификацией. Реализаций существует множество, и, как уже было замечено, они бывают открытыми и закрытыми. Самая популярная и «классическая» — это HotSpot от проекта OpenJDK, которая поддерживает JIT-компиляцию, оптимизированный алгоритм сборки мусора и имеет другие преимущества. Множество других VM создано на основе HotSpot, но с дополнительными функциями и улучшениями. Такие реализации тоже должны быть открытыми, бесплатными и проходить стандартные тесты от Oracle, но могут предлагать клиентам платные услуги в виде технической поддержки, консультаций и так далее.
Экземпляр
Экземпляр — это процесс, запущенный на основе реализации JVM. Этот процесс выполняет Java-программу. Другими словами, когда происходит запуск программы, создается экземпляр JVM, количество экземпляров равно количеству запущенных программ. Как только программа завершает свое выполнение, экземпляр тоже «исчезает». Каждый экземпляр работает изолированно и имеет собственную область памяти.
Загрузка и выполнение class-файлов в JVM
Поговорим о том, как именно виртуальная машина Java выполняет свою главную задачу — запуск и выполнение программ.
Загрузчик классов
Загрузчик классов (class loader) — это один из компонентов JVM, который осуществляет загрузку классов (обычно из class-файлов) в память во время исполнения программы. Существует так называемая иерархия загрузчиков.
Типы загрузчиков классов:
- Bootstrap Class Loader — это встроенный в VM и реализованный на машинном коде загрузчик, который скачивает основные классы Java, требуемые для работы JVM: java.lang, java.util и другие классы стандартной библиотеки. Этот загрузчик не имеет собственного объекта ClassLoader, проверить это можно следующим образом:
System.out.println(String.class.getClassLoader());
// null
Bootstrap Class Loader также не имеет родителя, так как является корневым загрузчиком в иерархии;
- Platform Class Loader — это загрузчик, который обеспечивает загрузку дополнительных классов, которые могут пригодиться разработчику. Ему видны все стандартные классы Java SE и JDK (это не означает, что все они будут загружены Platform Class Loader);
- Application Class Loader (System ClassLoader) — это загрузчик, который отвечает за загрузку всех классов из classpath (каталоги, JAR-файлы и т.д.), определенных при запуске приложения. Это последний загрузчик в иерархии.
Также существует несколько принципов загрузки классов:
- Делегирование. Здесь важно еще раз сказать про иерархию, которую можно изобразить так: Bootstrap Class Loader - > Platform Class Loader - > Application Class Loader. Суть принципа делегирования: когда загрузчик «обнаруживает» класс, он передает запрос на загрузку своему родителю, и только если родитель не может выполнить загрузку, загрузчик самостоятельно загружает класс. То есть, если Application Class Loader «обнаружил» класс, то сначала он передаст запрос на его загрузку Platform Class Loader и Bootstrap Class Loader. Если оба эти загрузчика не смогут найти и загрузить класс, то это сделает Application Class Loader.
- Видимость. Данный принцип состоит в том, что конкретный загрузчик может видеть только классы, который загрузил он сам или его родитель. Классы, загруженные загрузчиками, находящимися ниже в иерархии, он видеть не может.
- Уникальность. Суть этого принципа заключается в том, что данный класс может быть загружен только один раз. Если один и тот же класс будет загружен, например, двумя загрузчиками, то VM воспримет их как отдельные два класса, что может вызвать проблемы в работе программы. Этот принцип тесно связан с принципом делегирования.
Фазы загрузки классов:
- Загрузка (Loading) — это начальный этап. Загрузчик получает команду к поиску определенного класса либо от JVM, либо из программы. Далее загрузчик читает байт-код и создает объект класса или интерфейс.
- Связывание (Linking) — на этом этапе происходит ряд операций и преобразований над загруженным классом: верификация, подготовка и разрешение.
- Верификация (Verification) — байт-код класса проверяется на корректность, совместимость с JVM и соответствие с другими правилами, обеспечивающими надежность и безопасность выполнения программ. Если верификация не пройдена, то будет выброшено уведомление java.lang.VerifyError.
- Подготовка (Preparation) — выделяется память для статических переменных класса, которые инициализируются значениями по умолчанию.
- Разрешение (Resolution) — символические ссылки заменяются на прямые, чтобы во время исполнения программы все зависимости класса были доступны.
- Инициализация (Initialization) — здесь происходит инициализация переменных класса их начальными значениями.
После выполнения всех этих операций класс может быть использован.
Механизм выполнения
Когда загрузка классов завершена, движок начинает работу непосредственно над выполнением байт-кода, а если быть точнее — выполнением программы каждого класса.
Для выполнения программы байт-код необходимо преобразовать в машинный. Для этого JVM использует интерпретатор или JIT-компилятор. Итак, механизм выполнения состоит из двух основных компонентов:
- Интерпретатор считывает и выполняет инструкции построчно, но выполняет свои функции медленнее, чем JIT-компилятор. Также при каждом новом вызове данного метода потребуется новая интерпретация.
- JIT-компилятор задействуется, когда обнаруживается, что есть часто вызываемые методы. Тогда JIT-компилятор компилирует эти части программы и преобразует их в машинный код, который и используется при повторном вызове метода. Такой подход позволяет повысить производительность. При этом если в программе нет методов, которые вызываются часто, интерпретатор обеспечит более высокую производительность.
Управление системными ресурсами
JVM осуществляет эффективное управление системными ресурсами, и в первую очередь под ресурсами в данном случае подразумевается память. Во-первых, память в JVM разделена на несколько областей, что позволяет более эффективно управлять ею.
Основные области памяти:
- куча (Heap) — основная область памяти, где хранятся объекты;
- стек (Stack), который создается для каждого потока — место для хранения данных о вызовах метода, локальных переменных, некоторых результатах вычислений;
- область методов (Method Area) — здесь хранятся данные загруженных классов, например, значения констант, имена методов и другие;
- регистры процессора (PC Registers) — область памяти, которая хранит адрес текущей инструкции для каждого потока.
Во-вторых, JVM освобождает память за счет удаления неиспользуемых объектов с помощью различных сборщиков мусора, каждый из которых оптимизирован под определенный сценарий, что позволяет избежать утечек памяти.
Также виртуальная машина взаимодействует с операционной системой для планирования выделения потоков.
Аналоги Java Virtual Machine
JVM — это среда выполнения для программ, написанных на Java, но есть и другие виртуальные машины, которые предоставляют схожие функции для других языков программирования.
Рассмотрим некоторые из таких аналогов:
- фреймворк .NET от Microsoft и его версия .NET Core. Основным языком для работы в .NET является C#, но поддерживаются и менее популярные языки программирования: F#, Haskell и Visual Basic. Принцип работы схож с принципом работы JVM — программа компилируется в байт-код, который называется Common Intermediate Language (CIL), а затем исполняющая среда Common Language Runtime (CLR) выполняет его. Сначала этот фреймворк работал только в операционной системе Windows, но впоследствии была выпущена версия .NET Core, которая совместима с Windows, macOS и Linux. Таким образом, .NET Core является кроссплатформенной версией;
- Android Runtime (ART) и его предшественник Dalvik. ART — это среда выполнения для приложений на Android, разработанная компанией Google. Поддерживает Java, Kotlin и другие языки программирования, оптимизирована для многих устройств на Android и обеспечивает высокую производительность. В ART реализована предварительная компиляция, в отличие от Dalvik, в котором применялась JIT-компиляция. Также в ART улучшены механизмы сбора мусора и механизмы отладки, что делает этот движок более производительным и эффективным.
Перспективы развития JVM
На сегодня JVM еще не достигла предела развития. Работа над улучшениями продолжается непрерывно и во многом заключается во внедрении новых технологий, концепций и функций. Среди основных направлений и перспектив развития можно выделить следующие:
- Project Loom. Loom — это проект по внедрению виртуальных потоков (virtual threads). Каждый обычный поток привязан к потоку ОС (OS thread), а виртуальный поток — нет. То есть, реализуется поведение, при котором несколько виртуальных потоков могут работать поверх одного OS thread. Это полезно для создания многопоточных приложений, ведь количество потоков ОС ограничено. Подробнее о Loom можно узнать на профильных ресурсах, например, на JavaRush;
- Project Panama. Сутью этого проекта является улучшение взаимодействия Java и нативного кода;
- GraalVM. Данный проект включает в себя разработку высокопроизводительного комплекта разработчика (JDK);
- повышение уровня безопасности. Сюда входит разработка новых механизмов изоляции, поиск уязвимых мест и их исправление, другие меры по повышению безопасности JVM.