Заглядываем Под Капот Языка Java: Компиляция и Байт-Код
Мир программирования часто делим на компилируемые и интерпретируемые языки. Но что, если я скажу вам, что Java предлагает третий, более интригующий способ — магию байт-кода?
Вот как это работает:
Сначала у нас есть исходный код Java в виде файла с расширением .java.
Этот код проходит через компилятор, тщательно проверяясь на ошибки и преобразуясь в байт-код. Забудьте о машинном коде — байт-код здесь находится на первом плане.
Компилированный байт-код — это наш переходный язык, абстракция, которая не привязана к какой-либо платформе. Этот файл можно передать любому устройству, способному интерпретировать Java, и вот тут на сцену выходит виртуальная машина Java (JVM).
JVM, наш виртуальный маг, читает байт-код и превращает его в машинный код, который уже может запуститься на конкретной платформе. Вот так, благодаря этому хитрому танцу компиляции и интерпретации, Java обеспечивает гибкость и переносимость кода.
Так что добро пожаловать в захватывающий мир Java, где каждая строка кода — это не просто инструкция, а часть увлекательного процесса, где байт-код волшебным образом оживает на виртуальной сцене!
Презентацию с видео можно скачать на Patreon .
JVM – что это? Как устроена виртуальная машина Java
Виртуальная машина Java («Java Virtual Machine» — JVM) — это основная часть платформы Java Runtime Environment (JRE), которая интерпретирует байт-код Java для запуска программ.
Одним из наиболее значительных преимуществ использования является использование JVM для запуска программы Java в любой операционной среде. В её основе реализуется принцип WORA (Write once, run anywhere — «написал один раз, запускай везде»), который сильно упростил процессы разработки.
Как правило, считается, что JVM имеет двойное определение — техническое и неформальное.
Техническое: JVM — это спецификация программы, которая обеспечивает среду выполнения кода Java.
Неофициальное: JVM запускает приложения Java, c помощью настроенных параметров для управления всеми программными ресурсами.
Что делает виртуальная машина Java
JVM выполняет две основные функции:
- позволяет запускать Java-программы на любом устройстве или в любой операционной системе;
- даёт доступ к управлению памятью программ и её оптимизации.
Поскольку JVM — это хорошо известная среда выполнения со стандартизированной конфигурацией, мониторингом и управлением, она естественным образом подходит для контейнерной разработки с использованием таких технологий, как Docker.
Роль Java Virtual Machine
Теперь давайте более подробно рассмотрим место виртуальной машины Java в выполняемых процессах.
Если вы внимательно посмотрите на схему, добавленную выше, то будет несложно догадаться, что JVM образует слой между операционной системой и программами Java.
Это означает, что скомпилированная Java-программа будет связываться с Java Virtual Machine, а JVM будет общаться с операционной системой, являясь своего рода посредником между скомпилированными файлами классов и операционной системой.
Файл .class и байт-код
Когда дело доходит до выполнения программы, главное, что интересует виртуальную машину Java — это определённый формат файла – .class.
Файлы классов содержат наполовину скомпилированный код, называемый байт-кодом, который в свою очередь предоставляет JVM инструкции, таблицу символов и другую вспомогательную информацию.
Архитектура виртуальной машины Java
Понять, что такое виртуальная машина Java, будет немного проще, если познакомиться с её архитектурой и тем, как она работает. Поэтому важно рассмотреть строение JVM и особенности её частей.
Java Virtual Machine состоит из трёх отдельных компонентов:
- загрузчик классов;
- область памяти;
- исполнительный механизм.
Загрузчики классов отвечают за динамическую загрузку файлов .class в виртуальную машину Java и сохранения байт-кода в области метода JVM, о которой мы поговорим чуть позже.
Загрузчик классов Java бывает трёх типов:
- BootStrap ClassLoader (Загрузчик классов начальной загрузки) — это машинный код, который запускает операцию, когда её вызывает JVM. Его задача — загрузить классы из папки rt.jar.
- Extension ClassLoader (Загрузчик классов расширений) является дочерним элементом Bootstrap ClassLoader и загружает расширения основных классов Java из каталога jre/lib/ext или любого другого каталога, на который указывает java.ext.dirs.
- System ClassLoader (Системный загрузчик классов) загружает классы, найденные в переменной среды CLASSPATH, -classpath или параметре командной строки -cp.
- Область памяти
Область памяти или, как её ещё называют, область данных времени выполнения JVM состоит из 5 частей:
- Область метода предназначена для хранения данных файлов .class: например, метаданные, данные полей и методов, а также код метода. Область метода создаётся автоматически при запуске виртуальной машины, и для каждой ВМ существует только одна область метода.
- Область кучи. В куче хранятся все объекты и соответствующие им переменные экземпляра. Когда мы создаём новый экземпляр класса, он сразу же загружается в область кучи, которая остается единственной во время выполнения задачи.
- Область стека. В неё загружаются все локальные переменные, вызовы методов и частичные результаты.
- Регистры ПК. В регистре ПК хранится адреса виртуальных машин Java, выполняющих операцию в данный момент. В Java каждый поток получает свой собственный регистр ПК.
- Стеки нативных методов. Нативные методы — это методы, написанные на C или C++. Виртуальная машина JVM хранит стеки, которые поддерживают такие методы, с отдельным стеком собственных методов, выделенным для каждого потока.
- Исполнительный механизм
Этот тип программного обеспечения используется для тестирования оборудования и программного обеспечения. При этом он не сохраняет никакой информации о тестируемом продукте.
- интерпретатора;
- JIT-компилятора;
- сборщика мусора.
Перед выполнением программы интерпретатор и компилятор JIT («Just-in-time» — «точно в нужное время») преобразуют байт-код в машинные инструкции. Интерпретатор делает это построчно.
В тот момент, когда в сценарии обнаруживается повторяющийся код, к нему подключается JIT-компилятор для ускорения операции. Затем он компилирует байт-код и заменяет его собственным кодом. Такой процесс повышает производительность всей системы.
Но за что же в таком случае отвечает сборщик мусора? В некоторых других языках программирования (таких как C++) освобождение памяти от объектов без циклических ссылок зависит лишь от самого программиста. Однако в JVM этим занимается сборщик мусора, что оптимизирует использование памяти.
Важно отметить, что сборка мусора выполняется в JVM автоматически через определённые отрезки времени и не требует отдельного внимания специалистов. Конечно, этот процесс можно попробовать принудительно запустить, вызвав System.gc(), но нет никакой гарантии, что это сработает.
Заключение
Несмотря на то, что виртуальная машина Java изначально предназначалась только для запуска и выполнения программ Java, на сегодняшний день она способна поддерживать многие языки сценариев и программирования, что лишь укрепило популярность данной платформы.
В этой статье мы познакомились с виртуальной машиной Java, её архитектурой и основными компонентами, а также поговорили о месте JVM в запрашиваемых системой операциях.
Введение
Байт-код Java — это промежуточное представление Java-кода, которое выполняется виртуальной машиной Java (JVM). При компиляции Java-программы компилятор Java ( javac ) преобразует ее в байт-код, представляющий собой набор инструкций, которые JVM может понять и выполнить. Этот байт-код является платформонезависимым, то есть одна и та же Java-программа может выполняться на различных устройствах и в различных операционных системах, следуя принципу “пиши один раз, выполняй везде” (WORA).
Особенности байт-кода Java
Байт-код Java является важнейшим элементом в сфере программирования на Java. Он служит связующим звеном между высокоуровневым Java-кодом и низкоуровневыми операциями, выполняемыми в виртуальной машине Java (JVM). Рассмотрим подробно, что такое байт-код Java, как он устроен и зачем необходим Java-программам.
Что такое байт-код Java?
Байт-код Java — это продукт процесса компиляции исходного Java-кода. Когда вы пишете Java-программу и компилируете ее, компилятор Java ( javac ) не преобразует код непосредственно в машинный. Вместо этого он переводит его в промежуточную форму, называемую байт-кодом. Этот байт-код представляет собой набор инструкций, которые не являются человекочитаемыми, как Java-код, но менее сложны, чем машинный код.
Структура байт-кода
Длина каждой инструкции в байт-коде Java равна одному байту (откуда и происхождение термина “байт-код”). Однако за некоторыми инструкциями следуют дополнительные байты, представляющие собой операнды для инструкций. Инструкции байт-кода, разработанные для компактности и эффективности, работают на основе стековой архитектуры. В этом их отличие от большинства физических архитектур процессоров, основанных на регистрах.
Вот более подробный обзор структуры байт-кода.
- Опкод. Первый байт каждой инструкции называется опкодом. Этот байт указывает на операцию, которую необходимо выполнить.
- Операнды. За некоторыми инструкциями следует один или несколько байтов, которые выступают в качестве операндов. Этими операндами могут быть индексы, константы или ссылки, с которыми работает инструкция.
Байт-код и Java-стек
Байт-код Java работает на основе стековой архитектуры. Это означает, что большинство операций байт-кода связано с занесением элементов в стек или их извлечением из стека. Например, арифметическая операция, такая как сложение, в байт-коде проходит следующим образом: два верхних элемента выгружаются из стека, складываются, а затем результат загружается обратно в стек.
Пример с разбором байт-кода
Вернемся к предыдущему примеру и посмотрим, как Java-код переводится в байт-код:
int a = 5;
int b = 10;
int sum = a + b;
При компиляции эти строки Java-кода преобразуются в серию инструкций байт-кода, которые при просмотре с помощью такого инструмента, как javap , могут выглядеть следующим образом:
0: iconst_5
1: istore_1
2: bipush 10
4: istore_2
5: iload_1
6: iload_2
7: iadd
8: istore_3
Вот что происходит на каждом шаге.
- iconst_5 : целое значение 5 помещается в стек.
- istore_1 : верхнее целое число (5) из стека резервируется в первой локальной переменной (a).
- bipush 10 : байтовое значение 10 помещается в стек.
- istore_2 : верхнее целое число (10) из стека резервируется во второй локальной переменной (b).
- iload_1 и iload_2 : целые числа a и b загружаются в стек.
- iadd : два верхних целых числа выгружаются из стека, складываются и результат (сумма) загружается обратно в стек.
- istore_3 : результат из стека резервируется в третьей локальной переменной (сумма).
Значение байт-кода
Использование байт-кода является одной из ключевых особенностей, обеспечивающих кроссплатформенность Java. Поскольку байт-код является стандартным, платформонезависимым форматом, Java-программы могут выполняться на любом устройстве, оснащенном виртуальной машиной Java, которая понимает, как интерпретировать байт-код. Такая конструкция позволяет справиться со сложностями, связанными с различными архитектурами машин, что дает возможность разработчикам Java писать код по принципу WORA.
Роль виртуальной машины Java в разработке программ
Виртуальная машина Java (JVM) является краеугольным камнем платформонезависимых возможностей Java. Роль виртуальной машины Java выходит далеко за рамки простого выполнения Java-программ. Понимание работы JVM необходимо разработчикам Java, так как она влияет на запуск, оптимизацию и отладку Java-приложений.
Что такое JVM?
JVM — это абстрактная вычислительная машина, являющаяся составной частью среды выполнения Java (JRE). В отличие от физической машины, которая непосредственно выполняет машинный код, JVM интерпретирует и исполняет байт-код Java. Такая конструкция позволяет запускать Java-приложения на любом устройстве или операционной системе, где есть реализация JVM, что соответствует принципу WORA.
Основные функции JVM
JVM выполняет несколько важнейших функций при работе с Java-программой.
- Загрузка байт-кода. JVM загружает скомпилированный байт-код Java из файлов .class . Процесс загрузки также включает проверку формата байт-кода и определение его структурной целостности.
- Верификация байт-кода. Перед выполнением байт-код верифицируется на соответствие стандартам безопасности Java. На этом этапе проверяется наличие опасного кода, который может нарушить права доступа и нанести потенциальный вред системе.
- Выполнение. JVM выполняет байт-код. JVM может интерпретировать байт-код напрямую, преобразуя каждую инструкцию в машинный код по мере выполнения программы. В качестве альтернативы в современных реализациях JVM используется компиляция JIT (Just-In-Time — точно в срок), при которой байт-код компилируется в “родной” машинный код для повышения производительности.
- Управление памятью. JVM управляет выделением памяти для объектов и массивов Java. Виртуальная машина Java также заботится о сборке мусора, автоматически освобождая память, которая больше не используется.
- Предоставление среды выполнения. JVM предоставляет среду выполнения, включающую библиотеки и API, необходимые для работы Java-приложений. JVM также предоставляет среду выполнения, которая решает такие задачи, как организация потоков, синхронизация и управление ресурсами.
Виртуальная машина Java и экосистема Java
JVM — это не просто отдельная структура, а часть более крупной экосистемы Java. Эта экосистема включает:
- Java Development Kit (JDK) — набор инструментов для разработки Java-приложений.
- Java Runtime Environment (JRE) — среду выполнения и запуска Java-приложений. Сердцем JRE является JVM.
Различные реализации JVM
Существуют различные реализации JVM, каждая из которых обладает своими особенностями и оптимизациями. К ним относятся:
- HotSpot от Oracle — широко используемая JVM, отличающаяся своей высокой производительностью и возможностями мониторинга.
- OpenJ9 — проект Eclipse, известный малым объемом памяти и быстрым временем запуска.
- GraalVM — высокопроизводительная JVM, поддерживающая дополнительные языки, такие как JavaScript, Ruby и Python.
Эволюционирование JVM
За последние годы JVM претерпела значительные изменения. Современные JVM отличаются высокой степенью сложности и обладают расширенными возможностями для оптимизации производительности, такими как:
- адаптивная оптимизация;
- компиляторы, работающие по принципу JIT;
- сборщики мусора, настроенные на различные типы приложений и рабочих нагрузок.
Благодаря этим усовершенствованиям, Java-приложения становятся быстрее и эффективнее, не требуя внесения изменений в код приложения.
Манипулирование байт-кодом
Манипулирование байт-кодом Java — это продвинутая техника, имеющая существенное значение для разработки на Java, в частности, для оптимизации производительности, анализа программ и расширения возможностей языка. Она позволяет разработчикам изменять поведение скомпилированных Java-программ на более глубоком уровне, чем исходный код.
Что такое манипулирование байт-кодом?
Под манипулированием байт-кодом понимается процесс изменения или расширения байт-кода Java-программы после ее компиляции. Этот процесс может включать добавление, изменение или удаление инструкций из байт-кода. Такие манипуляции позволяют изменить поведение программы, добавить новые возможности или оптимизировать производительность без модификации исходного кода.
Инструменты и библиотеки для манипулирования байт-кодом
Для манипулирования байт-кодом в Java разработано несколько инструментов и библиотек.
- ASM. Низкоуровневый фреймворк для манипулирования и анализа байт-кода. ASM обеспечивает прямое манипулирование байт-кодом, предоставляя средства для анализа, создания и модификации скомпилированных классов Java.
- Javassist. Высокоуровневая библиотека манипулирования байт-кодом, позволяющая разработчикам работать с байт-кодом с помощью более простого API по сравнению с ASM. Она особенно полезна для динамической модификации классов во время выполнения программы.
- Byte Buddy. Относительно новая библиотека для создания и модификации Java-классов во время выполнения Java-приложения. Она сочетает в себе простоту использования и широкие возможности, позволяя разработчикам перехватывать вызовы методов, создавать прокси-классы и т. д.
Примеры использования манипуляций с байт-кодом
Манипулирование байт-кодом используется в различных сценариях.
- Оптимизация производительности. Средства профилирования и оптимизаторы производительности часто модифицируют байт-код для внедрения кода мониторинга или оптимизации “горячих” путей кода.
- Тестирование и отладка. Инструменты могут динамически вставлять средства логирования и отладки во время выполнения программы без внесения изменений в исходный код.
- Аспектно-ориентированное программирование (АОП). Такие фреймворки, как Spring, используют манипуляции с байт-кодом для реализации сквозных задач — логирования, управления транзакциями и проверки безопасности.
- Генерация кода во время выполнения. Библиотеки могут генерировать новые классы во время выполнения программы на основе динамических условий, что повышает гибкость и сокращает количество шаблонного кода.
Пример добавления логирования в метод
В качестве иллюстрации рассмотрим добавление логирования в метод. Не изменяя исходного кода, можно использовать библиотеку манипулирования байт-кодом для вставки логирования до и после вызова метода. Обычно этот процесс включает:
- загрузку байт-кода целевого класса;
- определение метода, в который необходимо добавить логирование;
- вставку инструкций байт-кода для выполнения операторов логирования;
- сохранение модифицированного байт-кода обратно в файл класса или загрузку его непосредственно в JVM.
Риски и ограничения
Манипулирование байт-кодом открывает широкие возможности, однако оно также связано с определенными рисками и ограничениями. Вот основные из них:
- Сложность. Манипулирование байт-кодом чревато ошибками и более сложно, чем работа с исходным кодом Java.
- Поддержка. Изменения, внесенные на уровне байт-кода, трудно отслеживать и поддерживать, особенно тем, кто не знаком со структурой байт-кода.
- Совместимость. Изменения байт-кода могут привести к нарушению совместимости с будущими версиями платформы Java, если это не будет сделано аккуратно.
Заключение
Байт-код Java — это мощный аспект языка программирования Java, обеспечивающий независимость от платформы и позволяющий использовать такие передовые технологии, как манипулирование во время выполнения. Понимание байт-кода и роли JVM необходимо всем Java-разработчикам, особенно тем, кто хочет глубже погрузиться во внутренние аспекты языка и его исполнения.
- Java и базы данных NoSQL: практическое руководство
- Обнаружение и предотвращение утечек памяти в Java
- 23 шаблона проектирования для 99% разработчиков на Java
Читайте нас в Telegram, VK и Дзен
«Виртуальная» машина Java – что это и с чем ее едят?
Программисты используют язык Java для формирования ПО разной сложности и задач. Но знают ли они, что происходит после начала компиляции готового кода?
Базовые понятия
Для простоты восприятия необходимо ввести несколько определений, которые не только приоткроют завесу таинственности, но и сократит дальнейшее повествование:
- Virtual Machine – несуществующее вычислительное устройство, реализуемое аппаратно, либо же программно. Процесс компиляции в исполняющие команды аналогичны работе микропроцессора.
- Java Platform – совокупность классов и VM составляют основу для платформы. Придает любой программе стандартный интерфейс вне зависимости от операционной системы.
- Java Virtual Machine – вычислительная машина, состоящая из набора регистров, базовых команд, стека и «хранилище». Основная задача – интерпретация кодинга ПО в байт-код.
- Java bytecode – разновидность машинного кода, который генерируется оригинальным компилятором. Выполнение прописанных команд происходит силами встроенного интерпретатора. Назван так из-за длины стандартной команды от JVM, а именно – 1 байт.
Почему именно «byte»?
JVM не требует путанной адресности для каждой ячейки памяти и большого количества регистров. Что это дает? Команды становятся короткими, легче и быстрее обрабатываются компилятором.
Программное обеспечение компилируется в код, который помещается в файлы специфического расширения. Таким образом реализуется главный принцип технологии, заложенный создателями: «Write on, run anywhere».
В чем секрет универсальности?
Абстрактная машина – маленький винтик до мелочей отлаженной среды исполнения Java Runtime Environment. Многие будут в шоке, узнав, что для каждой ОС и архитектуры ЦПУ нужна собственная JRE. В чем же секрет? «Переносимость» достигла нынешнего уровня только за счет множественных вариаций исполнительной среды. Стоит ли говорить, что запустить Java без JRE невозможно?
Популярности вагончик тронулся…
Кросс-платформенность, эффективность и безопасность сделали технологию Java востребованной. Да не просто нужной, а одной из самых популярных в мире. Сегодня более 2,5 млрд. устройств используют труды компании Sun Microsystem.
Всесторонняя разработка и улучшение сделали из локального успеха мировой бренд. Трудно найти человека, который бы не слышал слово «Java», пусть и из Minecraft-а. Если говорить о перспективах, то каждый год ставит перед разработчиками новые вызовы, отвечать на которые лучше кропотливой работой.