В языках программирования, таких как C и C++, код сначала компилируется в “машинный код” для конкретной платформы. Эти языки называются компилируемыми языками.
С другой стороны, для таких языков, как JavaScript и Python, необходим интерпретатор который выполняет инструкции напрямую, без необходимости компиляции. Эти языки называются интерпретируемыми.
Java использует комбинацию обоих методов. Код Java сначала компилируется в байтовый код и генерирует файл класса (.class
). Этот файл класса затем интерпретируется виртуальной машиной Java для базовой платформы. Один и тот же файл класса может выполняться на любой версии JVM, на любой платформе и операционной системе.
Подобно обычным виртуальным машинам, JVM создает изолированное пространство на хост-машине. Это пространство может использоваться для выполнения Java-программ независимо от платформы или операционной системы компьютера.
Существует единая спецификация о том, как должна быть реализована JVM чтобы быть универсальной.
Спецификация JVM описывает ожидаемые компоненты и поведение для любой JVM. Однако, она не предписывает конкретный подход к реализации этих компонентов, поэтому их существует множество разновидностей:
Ссылка на репозиторий в github
Это основная и самая популярная JVM которой пользуется большинство java-разработчиков.
«HotSpot», впервые выпущенная 27 апреля 1999 года, изначально разрабатывалась «Longview Technologies» — небольшой компанией, основанной в 1994 году. В 1997 году компанию купила Sun Microsystems. Сначала «HotSpot» использовали как дополнение к «Java 1.2», однако, эта виртуальная машина стала основной с выходом «Java 1.3».
Эта JVM называется «HotSpot» потому что, выполняя байт-кода «Java», она ищет его «горячие» места (англ. «hot spots») — многократно выполняющиеся. Поиск направлен на оптимизацию их выполнения: выделение им больших ресурсов вместе с уменьшением непроизводительных затрат для выполнения менее ресурсоёмкого кода.
Виртуальная машина «HotSpot» написана на «C++». Как указано на домашней странице «HotSpot», размер её исходного кода составляет 250 000 строк. «Hotspot» предоставляет следующую функциональность:
HotSpot на данный момент поддерживается Oracle в операционных системах Microsoft Windows, Linux и Solaris.
Разрабатывается Eclipse Foundation. Была создана компанией IBM. OpenJ9 лежит в основе многочисленных продуктов IBM Java, в том числе WebSphere Micro Edition, а также в качестве основы для всех IBM JDK, начиная с версии 5. IBM предоставила OpenJ9 VM в распоряжение проекта Apache Harmony для запуска их библиотеки классов.
OpenJ9 была разработана с расчётом на переносимость на различные платформы, а также масштабируемость от мобильных телефонов до мэйнфреймов zSeries.
Основана на HotSpot/OpenJDK и написанная на Java. GraalVM поддерживает разные языки программирования и модели выполнения, такие как JIT-компиляция и AOT-компиляция. Первая стабильная версия, 19.0, была выпущена в мае 2019-го года.
Существует несколько основных способов, которыми GraalVM может помочь с приложениями Java:
JVM можно разделить на несколько отдельных компонентов:
Процесс начинается с того, что загрузчик класса (далее, ClassLoader) получает задание найти определенный класс, что может быть инициировано самой JVM, или вызвано командой в вашем коде. Задача же здесь заключается в том, чтобы взять полное имя класса (например, java.lang.String
) и получить соответствующий файл класса (например, String.class
) из его местоположения (откуда угодно) —> в память JVM.
Отвечает за загрузку основных библиотек Java, расположенных в java.base модуле (java.lang, java.util и т.д.), необходимых для старта JVM.
Другие загрузчики классов написаны на Java (объекты java.lang.ClassLoader), что означает — их также необходимо загрузить в JVM! Эту задачу также выполняет Bootstrap ClassLoader.
Platform ClassLoader пришел на смену Extension ClassLoader, который искал в $JAVA_HOME/lib/ext, и использовался в Java 8 и более ранних версиях. Это изменение произошло с появлением Системы Модулей (JEP-261)
Простым языком можно сказать, что он загружает публичные типы системных модулей, которые могут понадобиться (Документация).
В пустой java программе он не загрузит ровным счетом ничего.
Также известный как системный загрузчик классов (System class loader). Именно этот загрузчик подгружает собственные реализации и библиотеки зависимостей, которые были переданы JVM (явно или неявно) при старте приложения в качестве -classpath (-cp) параметра.
Именно этот загрузчик является родителем основного потока приложения, и будет являться родителем ваших собственных загрузчиков классов, если вы решите реализовать один.
JVM позволяет создавать свои собственные, пользовательские загрузчики классов, непосредственно в своих Java программах, что обеспечивает независимость приложений.
Ошибки, которые будут брошены если класс не смог загрузиться ни одним из загрузчиков: NoClassDefFoundError или ClassNotFoundException
Линковка или связывание это процесс, который включает в себя следующие шаги:
Верификация происходит до исполнения байт кода. Это статический анализ, результаты которого гарантирует, что двоичное представление класса или интерфейса является структурно правильным. Полный список проверок описан в спецификации, параграф §4.9.
Несколько простых примеров:
boolean
, byte
, char
, short
или int
, можно использовать только инструкцию возврата ireturn
;Если проверка провалилась, то будет брошен VerifyError. Если хоть какой-то метод некорректный, то весь класс помечается неверефецированным и не исполнится никакой метод этого класса.
Подготовка включает в себя создание статических полей для класса или интерфейса и инициализацию таких полей значениями по умолчанию. Это не требует выполнения какого-либо кода виртуальной машины Java; явные инициализаторы для статических полей выполняются как часть инициализации, а не подготовки.
Предположим, что в классе объявлена следующая переменная:
private static final boolean enabled = true;
На этапе подготовки JVM выделяет память для переменной enabled
и устанавливает ее значение в значение по умолчанию для логического значения, которое равно false
.
Многие инструкции виртуальной машины Java полагаются на символические ссылки константного пула рантайма. Выполнение любой из этих инструкций (new
, instanceof
, invokedynamic
, invokespecial
, invokestatic
, ldc
, putstatic
, etc) требует разрешения символической ссылки.
Разрешение — это процесс динамического определения одного или нескольких конкретных значений из символической ссылки в пуле констант рантайма. Первоначально все символические ссылки в константном пуле неразрезолвены.
Инициализация включает выполнение метода инициализации класса или интерфейса (известного как операнд <clinit>
в байткод интерпретации). Сюда может входить вызов конструктора класса, выполнение статического блока и присвоение значений всем статическим переменным. Это заключительный этап загрузки класса. Подробнее процесс инициализации класса описан на странице Инициализация
К примеру, ранее мы объявили следующее:
private static final boolean enabled = true;
На этапе подготовки переменной enabled
было присвоено значение по умолчанию false
. На этапе инициализации этой переменной присваивается ее фактическое значение true
.
JVM может исполнять байткод двумя способами:
Ручное построчное исполнение операндов в байт коде JVM машиной. Из-за построчного выполнения интерпретатор работает сравнительно медленнее. Еще один недостаток интерпретатора — при многократном вызове метода каждый раз требуется новая интерпретация.
Компиляция в машинный код для исполнения на процессоре.
Компилирует горячий код, который определяется специальным динамическим профилировщиком. Он смотрит какой код чаще всего исполняется и компилирует его. Механизм выполнения сначала использует интерпретатор для выполнения байт-кода, но когда он находит какой-то повторяющийся код, то задействует JIT-компилятор. Затем JIT-компилятор компилирует весь байт-код и изменяет его на собственный машинный код. Этот собственный машинный код используется непосредственно для повторных вызовов методов, что повышает производительность системы.
Применяте различные оптимизации.
Компилирует код в машинный до исполнения программы
Применяет самые жесткие оптимизации
Позволяет получить доступ к классам, полям и методам по имени из Java программы.
Вся мета информация о классах хранится в Meta-Space, который пришел на смену PermGen.
Иногда необходимо задействовать в работе нативный (не Java) код (например, написанный на C/C++). К примеру, в тех случаях, когда нужно взаимодействовать с физическим оборудованием или преодолевать ограничения по управлению памятью и производительности в Java. Java поддерживает выполнение нативного кода через нативный интерфейс Java (JNI).
JNI действует как мост для предоставления вспомогательных пакетов другим языкам программирования, таким как C, C++ и так далее. Это особенно полезно в тех случаях, когда нужно написать код, который не полностью поддерживается Java, например, некоторые специфичные для платформы функции могут быть написаны только на C. (Метод класса Object hashcode() является нативным)
Вы можете воспользоваться ключевым словом native, чтобы указать, что реализация метода будет предоставлена нативной библиотекой. Также потребуется вызвать System.LoadLibrary(), чтобы загрузить общую нативную библиотеку в память и сделать ее функции доступными для Java.
Java поддерживает многопоточность. Каждая ОС на сегодняшний день имеет поддержку многопоточности и java переиспользует эти возможности осуществляя сопоставление своего потока с нативным.
Каждый поток, работающий в виртуальной машине Java, имеет свой собственный стек. Стек содержит информацию о том, какие методы вызвал поток (стек вызовов). Как только поток выполнит свой код, стек вызовов изменяется.
Стек потока содержит все локальные переменные для каждого выполняемого метода. Поток может получить доступ только к своему стеку. Локальные переменные, невидимы для всех других потоков, кроме потока, который их создал. Даже если два потока выполняют один и тот же код, они всё равно будут создавать локальные переменные этого кода в своих собственных стеках. Таким образом, каждый поток имеет свою версию каждой локальной переменной.
Все локальные переменные примитивных типов (boolean, byte, short, char, int, long, float, double) полностью хранятся в стеке потоков и не видны другим потокам. Один поток может передать копию примитивной переменной другому потоку, но не может совместно использовать примитивную локальную переменную.
Для реализации используются различные инструменты по типу volatile
или synchronized
инструкции.
Синхронизация довольно дорогостоящая оперцаия, но если отсутствует реальная конкуренция к доступу за данными, JVM умеет это оптимизировать и никакого оверхеда не будет.
Подробнее будет в статье Java Memory Model
Спецификация JVM явным образом не описывает как должна быть организована область памяти Heap (куча), поэтому это во многом зависит от того какой Garbage Collector используется. В разных реализациях может делиться на:
JVM запрашивает у ОС сразу большой кусок памяти, а потом делить его на Thread local heap - где каждый потом использует свой кусок памяти. Таким образом аллокация памяти в Java это довольно быстрая операция.
Что такое "мусор"? Мусором считается объект, который больше не может быть достигнут по ссылке из какого-либо объекта. Поскольку такие объекты больше не используются в приложении, то их можно удалить из памяти.
Что не является мусором?