Перейти к содержимому

Как называется двоичный формат который понимает виртуальная машина java

  • автор:

Виртуальная машина Java — Введение

У Oracle есть своя собственная реализация JVM (называемая JVM HotSpot), у IBM — своя (например, JVM J9).

Операции, определенные внутри спецификации, приведены ниже (источник — спецификации Oracle JVM, см. Ссылку выше) —

  • Формат файла ‘class’
  • Типы данных
  • Примитивные типы и значения
  • Типы ссылок и значения
  • Области данных времени выполнения
  • Рамки
  • Представление объектов
  • Арифметика с плавающей точкой
  • Специальные методы
  • Исключения
  • Сводка инструкций
  • Библиотеки классов
  • Публичный дизайн, частная реализация

JVM — это виртуальная машина, абстрактный компьютер, который имеет свой собственный ISA, собственную память, стек, кучу и т. Д. Он работает в операционной системе хоста и предъявляет ему требования к ресурсам.

Виртуальная машина Java — Архитектура

Архитектура HotSpot JVM 3 показана ниже —

Архитектура

Механизм выполнения состоит из сборщика мусора и JIT-компилятора. JVM поставляется в двух вариантах — клиент и сервер . Оба они используют один и тот же код времени выполнения, но отличаются тем, какой JIT используется. Мы узнаем больше об этом позже. Пользователь может контролировать, какой вариант использовать, указав флаги JVM -client или -server . Серверная JVM была разработана для долго работающих Java-приложений на серверах.

JVM поставляется в 32-битной и 64-битной версиях. Пользователь может указать, какую версию использовать, используя -d32 или -d64 в аргументах виртуальной машины. 32-битная версия может адресовать только до 4 ГБ памяти. С критическими приложениями, поддерживающими большие наборы данных в памяти, версия 64b отвечает этой потребности.

Виртуальная машина Java — загрузчик классов

JVM динамически управляет процессом загрузки, связывания и инициализации классов и интерфейсов. В процессе загрузки JVM находит двоичное представление класса и создает его.

В процессе связывания загруженные классы объединяются в состояние времени выполнения JVM, чтобы их можно было выполнить на этапе инициализации . JVM в основном использует таблицу символов, хранящуюся в постоянном пуле времени выполнения, для процесса связывания. Инициализация состоит из фактического выполнения связанных классов .

Типы погрузчиков

Загрузчик классов BootStrap находится на вершине иерархии загрузчиков классов. Он загружает стандартные классы JDK в каталог lib JRE.

Загрузчик класса Extension находится в середине иерархии загрузчика классов и является непосредственным потомком загрузчика классов начальной загрузки и загружает классы в каталог lib \ ext JRE.

Загрузчик классов приложений находится в нижней части иерархии загрузчиков классов и является непосредственным потомком загрузчика классов приложений. Он загружает файлы jar и классы, указанные в переменной CLVSPATH ENV .

соединение

Процесс связывания состоит из следующих трех шагов —

Проверка — это выполняется с помощью верификатора байт-кода, чтобы убедиться, что сгенерированные файлы .class (байт-код) действительны. Если нет, выдается ошибка, и процесс компоновки останавливается.

Подготовка — память выделяется для всех статических переменных класса, и они инициализируются со значениями по умолчанию.

Решение. Все символические ссылки в памяти заменяются исходными ссылками. Для этого используется таблица символов в постоянной памяти времени выполнения области метода класса.

инициализация

Это последний этап процесса загрузки классов. Статическим переменным присваиваются исходные значения и выполняются статические блоки.

Виртуальная машина Java — области данных времени выполнения

Спецификация JVM определяет определенные области данных времени выполнения, которые необходимы во время выполнения программы. Некоторые из них создаются во время запуска JVM. Другие являются локальными для потоков и создаются только при создании потока (и уничтожаются при разрушении потока). Они перечислены ниже —

ПК (счетчик программ) Регистрация

Он является локальным для каждого потока и содержит адрес инструкции JVM, которую поток выполняет в данный момент.

стек

Он является локальным для каждого потока и сохраняет параметры, локальные переменные и адреса возврата во время вызовов метода. Ошибка StackOverflow может возникнуть, если поток требует больше места в стеке, чем это разрешено. Если стек динамически расширяемый, он все равно может выбросить OutOfMemoryError.

отвал

Он распределяется между всеми потоками и содержит объекты, метаданные классов, массивы и т. Д., Которые создаются во время выполнения. Он создается при запуске JVM и уничтожается при выключении JVM. Вы можете контролировать объем кучи, требуемой вашей JVM от ОС, используя определенные флаги (подробнее об этом позже). Необходимо соблюдать осторожность, чтобы не потребовать слишком мало или слишком много памяти, поскольку это имеет важные последствия для производительности. Кроме того, GC управляет этим пространством и постоянно удаляет мертвые объекты, чтобы освободить пространство.

Область метода

Эта область выполнения является общей для всех потоков и создается при запуске JVM. Он хранит структуры для каждого класса, такие как пул констант (подробнее об этом позже), код для конструкторов и методов, данные методов и т. Д. JLS не определяет, нужно ли собирать эту область, и, следовательно, реализации JVM может игнорировать GC. Кроме того, это может или не может расширяться в соответствии с потребностями приложения. JLS не требует ничего в отношении этого.

Постоянный пул времени выполнения

JVM поддерживает структуру данных для каждого класса / типа, которая действует как таблица символов (одна из ее многочисленных ролей) при связывании загруженных классов.

Стеки родного метода

Когда поток вызывает собственный метод, он входит в новый мир, в котором структуры и ограничения безопасности виртуальной машины Java больше не ограничивают его свободу. Собственный метод может, вероятно, получить доступ к областям данных времени выполнения виртуальной машины (это зависит от интерфейса нативного метода), но также может делать все что угодно.

Вывоз мусора

JVM управляет всем жизненным циклом объектов в Java. Как только объект создан, разработчику больше не нужно беспокоиться об этом. В случае, если объект становится мертвым (то есть, на него больше нет ссылок), он извлекается из кучи GC, используя один из многих алгоритмов — последовательный GC, CMS, G1 и т. Д.

Во время процесса GC объекты перемещаются в память. Следовательно, эти объекты не могут быть использованы во время процесса. Все приложение должно быть остановлено на время процесса. Такие паузы называются паузами «останови мир» и являются огромными накладными расходами. Алгоритмы GC направлены в первую очередь на сокращение этого времени. Мы обсудим это очень подробно в следующих главах.

Благодаря GC, утечки памяти очень редки в Java, но они могут произойти. В следующих главах мы увидим, как создать утечку памяти в Java.

Виртуальная машина Java — JIT-компилятор

В этой главе мы узнаем о JIT-компиляторе и разнице между компилируемыми и интерпретируемыми языками.

Скомпилированные и интерпретированные языки

Такие языки, как C, C ++ и FORTRAN, являются скомпилированными языками. Их код поставляется в виде двоичного кода, предназначенного для базовой машины. Это означает, что высокоуровневый код компилируется в двоичный код одновременно статическим компилятором, написанным специально для базовой архитектуры. Созданный двоичный файл не будет работать на любой другой архитектуре.

С другой стороны, интерпретируемые языки, такие как Python и Perl, могут работать на любой машине, если у них есть действительный интерпретатор. Он переходит построчно к высокоуровневому коду, преобразуя его в двоичный код.

Интерпретируемый код обычно медленнее, чем скомпилированный код. Например, рассмотрим цикл. Интерпретируемый преобразует соответствующий код для каждой итерации цикла. С другой стороны, скомпилированный код сделает перевод только одним. Кроме того, поскольку интерпретаторы видят только одну строку за раз, они не могут выполнить какой-либо значимый код, например, изменить порядок выполнения операторов, таких как компиляторы.

Мы рассмотрим пример такой оптимизации ниже —

Добавление двух чисел, хранящихся в памяти . Поскольку доступ к памяти может занимать несколько циклов ЦП, хороший компилятор выдаст инструкции для извлечения данных из памяти и выполнения сложения только при наличии данных. Он не будет ждать, а пока выполнит другие инструкции. С другой стороны, никакая такая оптимизация была бы невозможна во время интерпретации, поскольку интерпретатор не знает весь код в любой момент времени.

Но тогда интерпретируемые языки могут работать на любой машине, у которой есть действительный интерпретатор этого языка.

Java компилируется или интерпретируется?

Ява пыталась найти золотую середину. Поскольку JVM находится между компилятором javac и базовым оборудованием, компилятор javac (или любой другой компилятор) компилирует код Java в байт-код, который понимается JVM для конкретной платформы. Затем JVM компилирует байт-код в двоичном формате, используя компиляцию JIT (Just-in-time) по мере выполнения кода.

HotSpots

В типичной программе часто выполняется только небольшая часть кода, и часто именно этот код существенно влияет на производительность всего приложения. Такие разделы кода называются HotSpots .

Если какой-то раздел кода выполняется только один раз, то его компиляция была бы пустой тратой усилий, и вместо этого было бы быстрее интерпретировать байт-код. Но если этот раздел является горячим разделом и выполняется несколько раз, JVM скомпилирует его. Например, если метод вызывается несколько раз, дополнительные циклы, которые потребуются для компиляции кода, будут компенсированы более быстрым генерируемым двоичным файлом.

Кроме того, чем больше JVM запускает конкретный метод или цикл, тем больше информации она собирает для проведения различных оптимизаций, чтобы генерировать более быстрый двоичный файл.

Давайте рассмотрим следующий код —

for(int i = 0 ; I  100; i++)  System.out.println(obj1.equals(obj2)); //two objects >

Если этот код интерпретируется, интерпретатор будет выводить для каждой итерации, что классы obj1. Это связано с тем, что у каждого класса в Java есть метод .equals (), который расширен от класса Object и может быть переопределен. Таким образом, даже если obj1 является строкой для каждой итерации, вывод все равно будет выполнен.

С другой стороны, в действительности JVM заметит, что для каждой итерации obj1 имеет класс String и, следовательно, будет генерировать код, соответствующий методу .equals () класса String. Таким образом, поиск не потребуется, и скомпилированный код будет выполняться быстрее.

Такое поведение возможно только тогда, когда JVM знает, как ведет себя код. Таким образом, он ждет, прежде чем скомпилировать определенные разделы кода.

Ниже приведен еще один пример —

int sum = 7; for(int i = 0 ; i  100; i++)  sum += i; >

Интерпретатор для каждого цикла извлекает из памяти значение «sum», добавляет к нему «I» и сохраняет его в памяти. Доступ к памяти является дорогостоящей операцией и обычно занимает несколько циклов ЦП. Поскольку этот код запускается несколько раз, это HotSpot. JIT скомпилирует этот код и выполнит следующую оптимизацию.

Локальная копия «sum» будет храниться в регистре, специфичном для конкретного потока. Все операции будут выполнены со значением в регистре, и когда цикл завершится, значение будет записано обратно в память.

Что если другие переменные также обращаются к переменной? Поскольку обновления выполняются в локальной копии переменной каким-либо другим потоком, они увидят устаревшее значение. В таких случаях необходима синхронизация потоков. Самым базовым примитивом синхронизации было бы объявление ‘sum’ как volatile. Теперь, прежде чем получить доступ к переменной, поток сбрасывает свои локальные регистры и извлекает значение из памяти. После доступа к нему значение сразу записывается в память.

Ниже приведены некоторые общие оптимизации, которые выполняются компиляторами JIT.

  • Метод встраивания
  • Устранение мертвого кода
  • Эвристика для оптимизации сайтов вызовов
  • Постоянное складывание

Виртуальная машина Java — уровни компиляции

JVM поддерживает пять уровней компиляции —

  • переводчик
  • C1 с полной оптимизацией (без профилирования)
  • C1 с счетчиками вызовов и задних кромок (легкое профилирование)
  • С1 с полным профилированием
  • C2 (использует данные профилирования из предыдущих шагов)

Используйте -Xint, если вы хотите отключить все JIT-компиляторы и использовать только интерпретатор.

Клиент против сервера JIT

Используйте -client и -server для активации соответствующих режимов.

Клиентский компилятор (C1) начинает компилировать код раньше, чем серверный компилятор (C2). Таким образом, к тому времени, когда C2 начал компиляцию, C1 уже скомпилировал части кода.

Но пока он ждет, C2 профилирует код, чтобы узнать о нем больше, чем C1. Следовательно, время ожидания, если смещение по оптимизации может быть использовано для создания гораздо более быстрого двоичного файла. С точки зрения пользователя, компромисс между временем запуска программы и временем, затраченным на ее запуск. Если время запуска является премиальным, то следует использовать C1. Если ожидается, что приложение будет работать в течение длительного времени (типично для приложений, развернутых на серверах), лучше использовать C2, поскольку он генерирует гораздо более быстрый код, который значительно компенсирует любое дополнительное время запуска.

Для таких программ, как IDE (NetBeans, Eclipse) и других программ с графическим интерфейсом, время запуска является критическим. Для запуска NetBeans может потребоваться минута или больше. Сотни классов компилируются при запуске таких программ, как NetBeans. В таких случаях компилятор C1 является лучшим выбором.

Обратите внимание, что существует две версии C1 — 32b и 64b . С2 приходит только в 64б .

Многоуровневая компиляция

В более старых версиях Java пользователь мог выбрать один из следующих параметров:

  • Переводчик (-Xint)
  • C1 (-клиент)
  • C2 (-сервер)

Он появился в Java 7. Он использует компилятор C1 для запуска и, когда код нагревается, переключается на C2. Его можно активировать с помощью следующих параметров JVM: -XX: + TieredCompilation. Значением по умолчанию является false в Java 7 и true в Java 8 .

Из пяти уровней компиляции ярусная компиляция использует 1 -> 4 -> 5 .

Виртуальная машина Java — 32b против 64b

На 32-битной машине может быть установлена ​​только 32-битная версия JVM. На 64-битной машине у пользователя есть выбор между 32-битной и 64-битной версиями. Но в этом есть определенные нюансы, которые могут повлиять на работу наших Java-приложений.

Если приложение Java использует менее 4 ГБ памяти, мы должны использовать 32-разрядную JVM даже на 64-разрядных компьютерах. Это связано с тем, что ссылки на память в этом случае были бы только 32b, и манипулирование ими было бы дешевле, чем манипулирование адресами 64b. В этом случае 64-битная JVM будет работать хуже, даже если мы используем OOPS (обычные объектные указатели). Используя OOPS, JVM может использовать 32-битные адреса в 64-битной JVM. Однако манипулирование ими будет медленнее, чем реальные ссылки 32b, поскольку базовые нативные ссылки будут по-прежнему 64b.

Если наше приложение будет использовать больше памяти 4G, нам придется использовать версию 64b, поскольку ссылки 32b могут адресовать не более 4G памяти. Мы можем установить обе версии на одном компьютере и переключаться между ними с помощью переменной PATH.

Виртуальная машина Java — оптимизация JIT

В этой главе мы узнаем об оптимизации JIT.

Метод Встраивания

В этой технике оптимизации компилятор решает заменить ваши вызовы функций на тело функции. Ниже приведен пример для того же —

int sum3; static int add(int a, int b)  return a + b; > public static void main(Stringargs)  sum3 = add(5,7) + add(4,2); > //after method inlining public static void main(Stringargs)  sum3 = 5+ 7 + 4 + 2; >

Используя эту технику, компилятор избавляет машину от накладных расходов при выполнении каких-либо вызовов функций (для этого необходимо перенести и перенести параметры в стек). Таким образом, сгенерированный код работает быстрее.

Встраивание метода может быть сделано только для не виртуальных функций (функций, которые не переопределяются). Подумайте, что произойдет, если метод add был переопределен в подклассе, а тип объекта, содержащего этот метод, неизвестен до времени выполнения. В этом случае компилятор не будет знать, какой метод встроить. Но если бы метод был помечен как ‘final’, то компилятор легко знал бы, что он может быть встроенным, потому что он не может быть переопределен каким-либо подклассом. Обратите внимание, что вовсе не гарантируется, что последний метод всегда будет встроен.

Недоступный и мертвый код

Недоступный код — это код, который не может быть достигнут ни одним из возможных потоков выполнения. Мы рассмотрим следующий пример —

void foo()  if (a) return; else return; foobar(a,b); //unreachable code, compile time error >

Мертвый код также является недостижимым кодом, но компилятор выдает ошибку в этом случае. Вместо этого мы просто получаем предупреждение. Каждый блок кода, такой как конструкторы, функции, try, catch, если, while и т. Д., Имеют свои собственные правила для недоступного кода, определенного в JLS (Спецификация языка Java).

Постоянное складывание

Чтобы понять концепцию постоянного сворачивания, см. Пример ниже.

final int num = 5; int b = num * 6; //compile-time constant, num never changes //compiler would assign b a value of 30.

Виртуальная машина Java — Сборка мусора

Жизненный цикл объекта Java управляется JVM. Как только объект создан программистом, нам не нужно беспокоиться об оставшейся части его жизненного цикла. JVM автоматически найдет те объекты, которые больше не используются, и освободит их память из кучи.

Сборка мусора — это основная операция, которую выполняет JVM, и ее настройка для наших нужд может значительно повысить производительность нашего приложения. Существуют различные алгоритмы сборки мусора, которые предоставляются современными JVM. Нам нужно знать о потребностях нашего приложения, чтобы решить, какой алгоритм использовать.

Вы не можете освобождать объект программно в Java, как вы можете делать в не-GC языках, таких как C и C ++. Таким образом, вы не можете иметь висячие ссылки в Java. Однако у вас могут быть нулевые ссылки (ссылки, которые относятся к области памяти, где JVM никогда не будет хранить объекты). Всякий раз, когда используется нулевая ссылка, JVM генерирует исключение NullPointerException.

Обратите внимание, что, хотя GC редко обнаруживает утечки памяти в программах Java, они случаются. Мы создадим утечку памяти в конце этой главы.

Следующие GC используются в современных JVM

  • Серийный коллектор
  • Пропускной коллектор
  • CMS коллектор
  • Коллектор G1

Каждый из вышеперечисленных алгоритмов выполняет одну и ту же задачу — находит объекты, которые больше не используются, и освобождает память, которую они занимают в куче. Один из наивных подходов к этому — подсчитать количество ссылок, которые есть у каждого объекта, и освободить его, как только число ссылок станет равным 0 (это также называется подсчетом ссылок). Почему это наивно? Рассмотрим круговой связанный список. Каждый из его узлов будет иметь ссылку на него, но на весь объект нигде нет ссылок, и в идеале его следует освободить.

JVM не только освобождает память, но и объединяет небольшие блоки памяти в большие. Это сделано для предотвращения фрагментации памяти.

Проще говоря, типичный алгоритм GC выполняет следующие действия:

  • Нахождение неиспользованных предметов
  • Освобождая память, которую они занимают в куче
  • Объединение фрагментов

GC должен остановить потоки приложения во время его работы. Это потому, что он перемещает объекты во время работы, и, следовательно, эти объекты не могут быть использованы. Такие остановки называются «паузами остановки мира», а минимизация частоты и продолжительности этих пауз — это то, к чему мы стремимся при настройке нашего GC.

Объединение памяти

Простая демонстрация объединения памяти показана ниже

Объединение памяти

Затененная часть — это объекты, которые необходимо освободить. Даже после того, как все пространство будет освобождено, мы можем выделить только объект с максимальным размером = 75 КБ. Это даже после того, как у нас есть 200 КБ свободного места, как показано ниже

Затененная часть

Виртуальная машина Java — GC поколения

Большинство JVM делят кучу на три поколения — молодое поколение (YG), старое поколение (OG) и постоянное поколение (также называемое постоянным поколением) . Каковы причины такого мышления?

Эмпирические исследования показали, что большинство созданных объектов имеют очень короткую продолжительность жизни —

Эмпирические исследования

Источник

Как вы можете видеть, чем больше объектов распределяется со временем, тем больше число оставшихся байтов становится меньше (в общем). Java-объекты имеют высокий уровень смертности.

Мы рассмотрим простой пример. Класс String в Java является неизменным. Это означает, что каждый раз, когда вам нужно изменить содержимое объекта String, вы должны создать новый объект в целом. Предположим, вы вносите изменения в строку 1000 раз в цикле, как показано в приведенном ниже коде —

String str = “G11 GC”; for(int i = 0 ; i

В каждом цикле мы создаем новый строковый объект, и строка, созданная во время предыдущей итерации, становится бесполезной (то есть на нее не ссылаются никакие ссылки). Время жизни этого объекта составило всего одну итерацию — они будут собраны GC в кратчайшие сроки. Такие недолговечные объекты хранятся в районе кучи молодого поколения. Процесс сбора предметов у молодого поколения называется второстепенной сборкой мусора, и он всегда вызывает паузу «мир-мир».

По мере того, как молодое поколение заполняется, GC выполняет незначительную сборку мусора. Мертвые объекты отбрасываются, а живые объекты переносятся в старое поколение. Потоки приложений останавливаются во время этого процесса.

Здесь мы видим преимущества, которые предлагает дизайн такого поколения. Молодое поколение — лишь малая часть кучи и быстро заполняется. Но обработка занимает намного меньше времени, чем обработка всей кучи. Таким образом, паузы «стоп-мир» в этом случае намного короче, хотя и чаще. Мы всегда должны стремиться к более коротким паузам, чем к более длинным, хотя они могут быть более частыми. Мы обсудим это подробно в следующих разделах этого руководства.

Молодое поколение делится на два пространства — райское и выжившее . Объекты, которые выжили во время сбора рая, перемещаются в пространство выживших, а те, кто выживает в пространстве выживших, переносятся в старое поколение. Молодое поколение уплотняется, пока его собирают.

Когда объекты перемещаются в старое поколение, оно со временем заполняется, и его необходимо собирать и уплотнять. Различные алгоритмы используют разные подходы к этому. Некоторые из них останавливают потоки приложения (что приводит к длительной паузе «остановка мира», поскольку старое поколение довольно велико по сравнению с молодым поколением), в то время как некоторые из них делают это одновременно с продолжением работы потоков приложения. Этот процесс называется полным GC. Два таких коллектора — CMS и G1 .

Давайте теперь проанализируем эти алгоритмы подробно.

Serial GC

это GC по умолчанию на компьютерах клиентского класса (однопроцессорные машины или 32-битная JVM, Windows). Как правило, GC сильно многопоточные, но последовательные GC нет. У него есть один поток для обработки кучи, и он останавливает потоки приложения всякий раз, когда он выполняет вспомогательный или основной сборщик мусора. Мы можем дать команду JVM использовать этот GC, указав флаг: -XX: + UseSerialGC . Если мы хотим использовать другой алгоритм, укажите имя алгоритма. Обратите внимание, что старое поколение полностью уплотняется во время крупного GC.

Пропускная способность GC

Этот GC используется по умолчанию на 64-битных виртуальных машинах и многопроцессорных компьютерах. В отличие от последовательного GC, он использует несколько потоков для обработки молодого и старого поколения. Из-за этого GC также называют параллельным коллектором . Мы можем дать команду нашей JVM использовать этот сборщик, используя флаг: -XX: + UseParallelOldGC или -XX: + UseParallelGC (для JDK 8 и выше). Потоки приложения останавливаются во время основной или вспомогательной сборки мусора. Как и серийный сборщик, он полностью уплотняет молодое поколение во время крупного ГХ.

Пропускная способность GC собирает YG и OG. Когда эден заполнен, коллектор выбрасывает из него живые объекты либо в OG, либо в одно из пространств выживших (SS0 и SS1 на диаграмме ниже). Мертвые объекты отбрасываются, чтобы освободить занимаемое ими пространство.

Перед ГК ЮГ

Перед ГК ЮГ

После GC из YG

После GC из YG

Во время полного GC коллектор пропускной способности очищает весь YG, SS0 и SS1. После операции OG содержит только живые объекты. Следует отметить, что оба вышеперечисленных коллектора останавливают потоки приложения при обработке кучи. Это означает длительные паузы «остановленного мира» во время крупного GC. Следующие два алгоритма направлены на их устранение за счет увеличения аппаратных ресурсов —

CMS Collector

Это означает «одновременный разметка». Его функция заключается в том, что он использует некоторые фоновые потоки для периодического сканирования старого поколения и избавляется от мертвых объектов. Но во время небольшого GC потоки приложения останавливаются. Однако паузы довольно маленькие. Это делает CMS сборщиком с низкой паузой.

Этому сборщику требуется дополнительное время ЦП для сканирования кучи во время работы потоков приложения. Кроме того, фоновые потоки просто собирают кучу и не выполняют никакого сжатия. Они могут привести к фрагментации кучи. Поскольку это продолжается, через определенный момент времени CMS остановит все потоки приложения и сожмет кучу, используя один поток. Используйте следующие аргументы JVM, чтобы сообщить JVM об использовании сборщика CMS:

«XX: + UseConcMarkSweepGC -XX: + UseParNewGC» в качестве аргументов JVM, указывающих использовать коллектор CMS.

До GC

До GC

После GC

После GC

Обратите внимание, что сбор выполняется одновременно.

G1 GC

Этот алгоритм работает путем разделения кучи на несколько областей. Как и коллектор CMS, он останавливает потоки приложения при выполнении вспомогательного GC и использует фоновые потоки для обработки старого поколения, сохраняя при этом потоки приложения. Поскольку оно делит старое поколение на регионы, оно продолжает их уплотнять, перемещая объекты из одного региона в другой. Следовательно, фрагментация минимальна. Вы можете использовать флаг: XX: + UseG1GC, чтобы сообщить вашей JVM об использовании этого алгоритма. Как и CMS, ему также нужно больше процессорного времени для обработки кучи и одновременной работы потоков приложений.

Этот алгоритм был разработан для обработки больших куч (> 4G), которые разделены на несколько различных областей. Некоторые из этих регионов составляют молодое поколение, а остальные — старое. YG очищается с использованием традиционно — все потоки приложения останавливаются и все объекты, которые все еще живы для старого поколения или пространства выживших.

Обратите внимание, что все алгоритмы GC разделяют кучу на YG и OG и используют STWP для очистки YG. Этот процесс обычно очень быстрый.

Виртуальная машина Java — настройка GC

В последней главе мы узнали о различных поколениях Gcs. В этой главе мы обсудим, как настроить GC.

Размер кучи

Размер кучи является важным фактором производительности наших Java-приложений. Если он слишком мал, он будет часто заполняться и, как следствие, GC должен будет часто собирать его. С другой стороны, если мы просто увеличим размер кучи, хотя ее нужно собирать реже, длина пауз увеличится.

Кроме того, увеличение размера кучи имеет серьезные последствия для базовой ОС. Используя подкачку страниц, ОС заставляет наши прикладные программы видеть намного больше памяти, чем фактически доступно. Операционная система управляет этим путем использования некоторого пространства подкачки на диске, копируя в него неактивные части программ. Когда эти части нужны, ОС копирует их обратно с диска в память.

Предположим, что у машины 8 ГБ памяти, а JVM видит 16 ГБ виртуальной памяти, JVM не знает, что на самом деле в системе доступно только 8 ГБ. Он просто запросит 16G у ОС и, как только получит эту память, продолжит ее использовать. Операционная система должна будет обмениваться большим количеством данных, и это является огромным снижением производительности системы.

И затем наступают паузы, которые будут происходить во время полного GC такой виртуальной памяти. Поскольку сборщик мусора и сборщик мусора будут обрабатывать всю кучу, ему придется много ждать, пока виртуальная память будет выгружена с диска. В случае одновременного сборщика фоновые потоки должны будут много ждать, пока данные будут скопированы из пространства подкачки в память.

Таким образом, здесь возникает вопрос о том, как мы должны определить оптимальный размер кучи. Первое правило — никогда не запрашивать у ОС больше памяти, чем есть на самом деле. Это полностью предотвратит проблему частой замены. Если на машине установлено и работает несколько JVM, то общий запрос памяти для всех из них меньше, чем фактический объем ОЗУ в системе.

Вы можете контролировать размер запроса памяти JVM, используя два флага:

  • -XmsN — контролирует начальную запрошенную память.
  • -XmxN — управляет максимальным объемом памяти, который может быть запрошен.

-XmsN — контролирует начальную запрошенную память.

-XmxN — управляет максимальным объемом памяти, который может быть запрошен.

Значения по умолчанию обоих этих флагов зависят от базовой ОС. Например, для 64-битных JVM, работающих на MacOS, -XmsN = 64M и -XmxN = минимум 1G или 1/4 от общей физической памяти.

Обратите внимание, что JVM может автоматически настраиваться между двумя значениями. Например, если он замечает, что происходит слишком много GC, он будет продолжать увеличивать объем памяти, пока он находится ниже -XmxN и желаемые цели производительности достигнуты.

Если вы точно знаете, сколько памяти требуется вашему приложению, вы можете установить -XmsN = -XmxN. В этом случае JVM не нужно вычислять «оптимальное» значение кучи, и, следовательно, процесс GC становится немного более эффективным.

Размеры поколения

Вы можете решить, какую часть кучи вы хотите выделить для YG, и какую часть вы хотите выделить для OG. Оба эти значения влияют на производительность наших приложений следующим образом.

Если размер YG очень большой, то он будет собираться реже. Это приведет к тому, что меньшее количество объектов будет переведено в OG. С другой стороны, если вы слишком сильно увеличите размер OG, то сбор и сжатие займет слишком много времени, и это приведет к длительным паузам STW. Таким образом, пользователь должен найти баланс между этими двумя значениями.

Ниже приведены флаги, которые вы можете использовать для установки этих значений —

  • -XX: NewRatio = N: отношение YG к OG (значение по умолчанию = 2)
  • -XX: NewSize = N: начальный размер YG
  • -XX: MaxNewSize = N: максимальный размер YG
  • -XmnN: установить NewSize и MaxNewSize на одно и то же значение, используя этот флаг

-XX: NewRatio = N: отношение YG к OG (значение по умолчанию = 2)

-XX: NewSize = N: начальный размер YG

-XX: MaxNewSize = N: максимальный размер YG

-XmnN: установить NewSize и MaxNewSize на одно и то же значение, используя этот флаг

Первоначальный размер YG определяется значением NewRatio по заданной формуле —

 (общий размер кучи) / (newRatio + 1) 

Поскольку начальное значение newRatio равно 2, в приведенной выше формуле начальное значение YG составляет 1/3 от общего размера кучи. Вы всегда можете переопределить это значение, явно указав размер YG с помощью флага NewSize. Этот флаг не имеет никакого значения по умолчанию, и если он не установлен явно, размер YG будет рассчитываться по формуле выше.

Permagen и Metaspace

Permagen и metaspace являются областями кучи, где JVM хранит метаданные классов. Пространство называется «permagen» в Java 7, а в Java 8 оно называется «metaspace». Эта информация используется компилятором и средой выполнения.

Вы можете контролировать размер пермагена, используя следующие флаги: -XX: PermSize = N и -XX: MaxPermSize = N. Размер Metaspace можно контролировать с помощью: -XX: Metaspace- Size = N и -XX: MaxMetaspaceSize = N.

Существуют некоторые различия в управлении permagen и metaspace, когда значения флага не установлены. По умолчанию оба имеют начальный размер по умолчанию. Но хотя метапространство может занимать столько кучи, сколько необходимо, permagen может занимать не более начальных значений по умолчанию. Например, 64-битная виртуальная машина Java имеет 82M пространства кучи в качестве максимального размера пермагена.

Обратите внимание, что, поскольку метапространство может занимать неограниченное количество памяти, если не указано иное, может быть ошибка нехватки памяти. Полный GC происходит всякий раз, когда размеры этих регионов изменяются. Следовательно, во время запуска, если загружается много классов, метапространство может продолжать изменять размер, каждый раз получая полный GC. Таким образом, запуск больших приложений занимает много времени в случае, если начальный размер метапространства слишком мал. Хорошей идеей является увеличение начального размера, так как это сокращает время запуска.

Хотя permagen и metaspace содержат метаданные класса, они не являются постоянными, и пространство восстанавливается GC, как в случае объектов. Это обычно в случае серверных приложений. Всякий раз, когда вы делаете новое развертывание на сервере, старые метаданные должны быть очищены, так как новым загрузчикам классов теперь потребуется место. Это пространство освобождается GC.

Виртуальная машина Java — утечка памяти в Java

Мы обсудим концепцию утечки памяти в Java в этой главе.

Следующий код создает утечку памяти в Java —

void queryDB()  try Connection conn = ConnectionFactory.getConnection(); PreparedStatement ps = conn.preparedStatement("query"); // executes a SQL ResultSet rs = ps.executeQuery(); while(rs.hasNext())  //process the record > > catch(SQLException sqlEx)  //print stack trace > >

В приведенном выше коде при выходе из метода мы не закрыли объект подключения. Таким образом, физическое соединение остается открытым до запуска GC и видит объект соединения как недостижимый. Теперь он вызовет последний метод для объекта подключения, однако он может быть не реализован. Следовательно, объект не будет мусором в этом цикле.

То же самое будет происходить в следующем, пока удаленный сервер не увидит, что соединение было открыто в течение длительного времени, и принудительно прервет его. Таким образом, объект без ссылки остается в памяти в течение длительного времени, что создает утечку.

Устройство и работа JVM (Java Virtual Machine)

Java Virtual Machine, или JVM, является важной частью Java-платформы и представляет собой виртуальное окружение, в котором запускаются Java-приложения. В этой статье мы разберемся, как устроена и работает JVM простым и понятным языком.

Что такое JVM?

JVM (Java Virtual Machine) — это виртуальная машина, обеспечивающая выполнение Java-приложений. Она является независимой от операционной системы, что позволяет Java-приложениям работать на любой платформе, имеющей JVM. Основная задача JVM — конвертировать байт-код Java в машинный код, который затем исполняется процессором компьютера.

Как устроена JVM?

JVM состоит из нескольких основных компонентов:

  1. Класс-лоадер (Class Loader) — загружает и инициализирует классы Java, преобразуя их из байт-кода в машинный код для выполнения.
  2. Память JVM (JVM Memory) — разделяется на несколько областей, таких как куча (Heap), стек (Stack), область методов (Method Area) и статическая область данных (Static Data Area).
  3. Сборщик мусора (Garbage Collector) — автоматически удаляет ненужные объекты из памяти, предотвращая утечки памяти.
  4. Исполнительный движок (Execution Engine) — интерпретирует и выполняет байт-код, преобразуя его в машинный код.

Как работает JVM (Java Virtual Machine)

Java Virtual Machine (JVM) — это виртуальная машина, которая обеспечивает исполнение Java-приложений. JVM выполняет байт-код, который является промежуточным представлением Java-кода после компиляции исходного кода. Рассмотрим детально процесс работы JVM:

  1. Загрузка классов (Class Loading): JVM загружает классы приложения в свою память в процессе, который называется загрузкой классов. Загрузчик классов (ClassLoader) считывает байт-код из файлов .class и загружает их в область методов (Method Area) JVM. Загрузка классов может происходить динамически, то есть по мере необходимости в процессе выполнения приложения.
  2. Проверка байт-кода (Bytecode Verification): после загрузки классов, JVM проверяет корректность и безопасность байт-кода, чтобы убедиться, что он соответствует спецификации Java и не содержит вредоносного кода. Это делается с помощью компонента, называемого верификатором байт-кода (Bytecode Verifier).
  3. Инициализация классов (Class Initialization): JVM выполняет инициализацию статических полей и блоков инициализации классов. Этот процесс включает присвоение начальных значений статическим переменным и выполнение статических блоков инициализации в порядке их объявления в исходном коде.
  4. Исполнение байт-кода (Bytecode Execution): после инициализации классов, JVM начинает исполнение байт-кода. Байт-код интерпретируется и выполняется с помощью компонента, называемого исполнителем байт-кода (Bytecode Interpreter). В некоторых случаях, JVM может использовать технологию Just-In-Time (JIT) компиляции для трансляции байт-кода в машинный код, который затем выполняется непосредственно процессором. Это может существенно улучшить производительность приложения.
  5. Сборка мусора (Garbage Collection): в процессе выполнения приложения, JVM автоматически освобождает память, занимаемую неиспользуемыми объектами, с помощью процесса, называемого сборкой мусора (Garbage Collection). Сборка мусора помогает предотвратить утечки памяти и обеспечивает эффективное использование памяти.

Память JVM

Память в JVM состоит из 5 основных участков:

  1. Куча (Heap)
  2. Стек (Stack)
  3. Область методов (Method Area) и Meta Space
  4. Регистры нативных методов (Native Method Stacks)
  5. Пул строк (String Pool)

Рассмотрим каждый из них более детально.

Куча (Heap)

Куча — это область памяти, где хранятся объекты и их данные. Она состоит из двух областей: молодого поколения (Young Generation) и старого поколения (Old Generation).

  • Молодое поколение состоит из трех частей: одной области Eden и двух областей Survivor Space (S0 и S1). Все новые объекты сначала создаются в области Eden. После процесса сборки мусора (Garbage Collection), выжившие объекты перемещаются между областями Survivor Space.
  • Старое поколение предназначено для хранения объектов, которые прожили достаточно долго и не были удалены во время предыдущих процессов сборки мусора. Объекты из молодого поколения могут быть перемещены в старое поколение, если они продолжают существовать после нескольких сборок мусора.

Стек (Stack)

Стек — это область памяти, где хранятся локальные переменные и ссылки на объекты, а также информация о вызовах методов. Для каждого потока (Thread) выделяется свой стек. Стек организован в виде последовательности стековых фреймов (Stack Frames), каждый из которых соответствует вызову метода.

Stack и Stack Frame имеют разные значения и используются в контексте управления памятью и выполнения программы в Java:

  1. Stack: Стек – это область памяти, выделенная для каждого потока в Java-приложении. Стек используется для хранения стековых фреймов, соответствующих вызовам методов, выполняемых в потоке. Стек работает по принципу LIFO (Last In, First Out), то есть последний добавленный элемент будет первым извлеченным. Когда поток вызывает метод, на стек добавляется новый стековый фрейм, а когда метод завершает выполнение, соответствующий стековый фрейм удаляется из стека.
  2. Stack Frame: Стековый фрейм – это структура данных, представляющая вызов одного метода в потоке выполнения. Каждый стековый фрейм содержит информацию о вызываемом методе, такую как локальные переменные, ссылки на операнды и ссылку на вызываемый метод. Когда метод вызывается, создается новый стековый фрейм, который размещается на вершине стека, а когда метод завершает выполнение, соответствующий стековый фрейм удаляется из стека.

Вкратце, стек – это область памяти, которая хранит стековые фреймы для каждого вызова метода в потоке, а стековый фрейм – это структура данных, содержащая информацию о вызове метода.

Область методов (Method Area) и Meta Space

Область методов хранит структуру классов, такую как метаданные классов, пул констант, статические переменные и код методов. Она разделяется между всеми потоками, работающими в JVM.

Meta Space является частью области методов и хранится в нативной памяти, а не в куче. Meta Space заменило PermGen (Permanent Generation) начиная с Java 8 и было введено для улучшения управления памятью и предотвращения переполнения PermGen, которое возникало в более ранних версиях Java.

Meta Space хранит следующие данные:

  1. Метаданные классов: это информация о структуре классов, такая как имена классов, имена полей, имена методов, модификаторы доступа и т. д.
  2. Пул констант: это набор значений, используемых в байт-коде, включая числовые константы, ссылки на классы и методы, строковые литералы и т. д.
  3. Статические переменные: это переменные, которые связаны с классом, а не с его экземплярами. Они имеют одно значение для всех экземпляров класса и сохраняют свое значение между вызовами методов.
  4. Код методов: это скомпилированный байт-код для методов класса.

Сборщик мусора (Garbage Collector) также работает в области Meta Space, удаляя метаданные классов, которые больше не используются приложением. Это помогает предотвратить утечки памяти и обеспечивает эффективное использование памяти JVM.

Регистры нативных методов (Native Method Stacks)

Native Method Stack — это область памяти, выделенная для каждого потока в приложении Java, в которой хранятся стековые фреймы для вызовов нативных методов, то есть методов, написанных на других языках программирования, таких как C или C++. Эти методы обычно используются для выполнения низкоуровневых операций или взаимодействия с системными ресурсами, которые недоступны или сложны для реализации средствами самого языка Java.

Нативные методы объявляются в Java с помощью ключевого слова native , и их реализации предоставляются в виде библиотек совместимых с платформой (например, DLL-файлы в Windows или .so-файлы в Unix-подобных системах). Обычно нативные методы используются в стандартной библиотеке Java (например, в пакете java.lang или java.io ) для обеспечения доступа к операционной системе и аппаратному обеспечению.

Когда Java-приложение вызывает нативный метод, для этого вызова создается стековый фрейм на Native Method Stack. Этот стековый фрейм аналогичен стековому фрейму на обычном стеке (Java Stack), но хранит информацию, специфичную для вызова нативного метода. Native Method Stack управляется JVM и используется для разделения памяти между потоками при вызове нативных методов.

Важно отметить, что Native Method Stack и Java Stack — это две разные области памяти, и они хранят разные типы стековых фреймов: стековые фреймы для вызовов методов, написанных на языке Java, хранятся на Java Stack, в то время как стековые фреймы для вызовов нативных методов хранятся на Native Method Stack.

Пул строк (String Pool)

Это механизм, используемый в JVM для оптимизации использования памяти при работе со строками. Это специальный кэш строк, который позволяет нескольким объектам String с одинаковым содержимым ссылаться на один и тот же объект String в куче памяти.

При создании объекта String в Java, JVM сначала проверяет String Pool на наличие уже существующего объекта с таким же содержимым. Если объект уже существует в String Pool, то новый объект String не создается, а ссылается на уже существующий объект. Это позволяет сократить использование памяти и повысить производительность, особенно при работе со строками с повторяющимся содержимым.

Строки, которые могут быть добавлены в String Pool, включают строки, созданные с помощью литералов (например, “hello”), а также строки, созданные с помощью метода String.intern(). Метод intern() возвращает ссылку на строку в String Pool, если такая строка уже существует, иначе он добавляет новую строку в String Pool.

До Java 7 (включительно)String Pool хранился в области памяти, называемой PermGen (Permanent Generation), который позже был заменен на Metaspace. Однако, начиная с Java 8 String Pool был перемещен в кучу.

Важно понимать, что хранение строк в String Pool может привести к утечкам памяти, если не управлять строковыми объектами правильно. Например, создание множества строк, которые необходимы только во время выполнения, может привести к переполнению String Pool и, как следствие, к утечке памяти. Чтобы избежать этого, следует быть внимательным при использовании метода intern() и не добавлять в String Pool строки, которые не нужны во время выполнения.

Также важно отметить, что в некоторых случаях использование String Pool может привести к неожиданным результатам при сравнении строк. Например, если две строки создаются с помощью конструктора String, они не будут добавлены в String Pool и не будут считаться одинаковыми, даже если содержат одинаковое значение. В этом случае необходимо использовать метод equals() для сравнения строк.

Для управления размером String Pool можно использовать параметры JVM, такие как -XX:StringTableSize и -XX:StringTableSizePerBucket. Они позволяют задать максимальный размер String Pool и количество корзин, в которые разбивается String Pool для улучшения производительности.

В целом, использование String Pool может существенно улучшить производительность и уменьшить использование памяти при работе со строками. Однако, необходимо быть внимательным при использовании этого механизма и управлять строковыми объектами правильно, чтобы избежать утечек памяти и неожиданных результатов.

Эти области памяти вместе обеспечивают эффективное выполнение Java-приложений, оптимизацию использования памяти и гибкость при работе с различными типами данных и структурами.

Сборка мусора (Garbage Collection, GC)

Это процесс автоматического освобождения памяти, занимаемой неиспользуемыми объектами в куче (Heap) JVM. Важной целью GC является эффективное использование памяти и предотвращение утечек памяти. Сборка мусора в JVM работает по следующему алгоритму:

  1. Определение мусора: сборщик мусора ищет объекты, на которые не существует ссылок из активных частей программы. Это означает, что объекты, которые больше не могут быть достигнуты и использованы, считаются мусором.
  2. Пометка (Marking): сборщик мусора начинает процесс пометки с корневых объектов (root objects). Корневые объекты — это объекты, на которые существуют прямые ссылки из стека (Stack) или статических переменных. Затем сборщик мусора продолжает процесс пометки, исследуя все связанные объекты рекурсивно. В результате, все достижимые объекты помечаются.
  3. Удаление (Sweeping): после процесса пометки, сборщик мусора удаляет все непомеченные объекты, освобождая память, которую они занимают. В зависимости от алгоритма сборки мусора, этот процесс может включать компактизацию памяти, то есть перемещение выживших объектов в куче, чтобы уменьшить фрагментацию памяти и улучшить производительность.

Существует несколько алгоритмов сборки мусора, которые используются в разных ситуациях и вариантах JVM. Некоторые из наиболее распространенных алгоритмов включают:

  1. Serial Garbage Collector
  2. Parallel Garbage Collector
  3. Concurrent Mark and Sweep (CMS)
  4. Garbage First (G1)

Serial Garbage Collector (GC)

Это алгоритм сборки мусора, который использует один поток для выполнения сборки мусора. Этот алгоритм подходит для небольших приложений с ограниченными требованиями к производительности и системами с ограниченными ресурсами, такими как маломощные серверы или встроенные системы. Serial GC идеально подходит для однопроцессорных систем или систем, где другие ресурсы процессора могут быть лучше использованы приложением.

Serial GC работает в двух основных фазах:

  1. Минорная сборка мусора (Minor GC): Минорная сборка мусора происходит в молодом поколении (Young Generation), которое состоит из областей Eden и Survivor. Новые объекты создаются в области Eden. Когда область Eden заполняется, Serial GC выполняет “Stop-The-World” операцию, останавливая все потоки приложения, и перемещает выжившие объекты из области Eden в одну из областей Survivor (называемую “to-space”). Объекты, которые не могут быть достигнуты, считаются мусором и удаляются. Затем область Eden очищается, и выполнение приложения возобновляется.
  2. Мажорная сборка мусора (Major GC) или полная сборка мусора (Full GC): Мажорная сборка мусора происходит в старшем поколении (Old Generation) и молодом поколении. Этот тип сборки мусора менее частый, но более затратный по времени. Мажорная сборка мусора также является “Stop-The-World” операцией, останавливая все потоки приложения. Во время мажорной сборки мусора Serial GC удаляет недостижимые объекты и перемещает выжившие объекты в старшее поколение.

Несмотря на свою простоту и эффективность в ситуациях с ограниченными ресурсами, Serial GC имеет недостатки. Основной из них — остановка работы приложения во время сборки мусора, что может привести к существенным задержкам, особенно при больших объемах памяти или долгоживущих объектах. В многопроцессорных системах Serial GC также может быть менее эффективным, чем многопоточные алгоритм

Parallel Garbage Collector (GC)

Он также известен как Throughput Collector, — это алгоритм сборки мусора, который использует несколько потоков для повышения производительности сборки мусора. Он подходит для многопроцессорных систем и приложений, которым требуется высокая пропускная способность (throughput) и быстрое освобождение памяти. Parallel GC обеспечивает более быстрое выполнение сборки мусора за счет использования нескольких ядер процессора, но при этом все еще использует “Stop-The-World” паузы.

Parallel GC работает в двух основных фазах:

  1. Минорная сборка мусора (Minor GC): Минорная сборка мусора происходит в молодом поколении (Young Generation), состоящем из областей Eden и Survivor. Когда область Eden заполняется, Parallel GC выполняет “Stop-The-World” операцию, останавливая все потоки приложения. В отличие от Serial GC, Parallel GC использует несколько потоков для сборки мусора. Выжившие объекты перемещаются из области Eden в одну из областей Survivor (называемую “to-space”), а недостижимые объекты удаляются. Затем область Eden очищается, и выполнение приложения возобновляется.
  2. Мажорная сборка мусора (Major GC) или полная сборка мусора (Full GC): Мажорная сборка мусора происходит в старшем поколении (Old Generation) и молодом поколении. Этот тип сборки мусора менее частый, но более затратный по времени. Мажорная сборка мусора также является “Stop-The-World” операцией, останавливая все потоки приложения. Во время мажорной сборки мусора Parallel GC использует несколько потоков для удаления недостижимых объектов и перемещения выживших объектов в старшее поколение.

Parallel GC позволяет улучшить производительность сборки мусора на многопроцессорных системах, однако его использование может повысить нагрузку на процессор из-за параллельной работы. Важно отметить, что, хотя Parallel GC ускоряет сборку мусора, он все равно использует “Stop-The-World” паузы, что может вызывать задержки в приложении.

Concurrent Mark and Sweep (CMS)

Concurrent Mark and Sweep (CMS) Garbage Collector (GC) — это алгоритм сборки мусора, разработанный для уменьшения пауз на сборку мусора в приложениях с низкой задержкой. Он предназначен для систем с большим объемом памяти и большим количеством ядер процессора. Основная идея CMS GC заключается в выполнении большей части работы по сборке мусора параллельно с работой приложения, чтобы уменьшить влияние на его производительность.

CMS GC состоит из следующих этапов:

  1. Инициализация (Initial Mark): на этом этапе сборщик мусора помечает корневые объекты (объекты, на которые есть прямые ссылки из стека или статических переменных). Этот этап является “Stop-The-World”, так как приложение приостанавливается на время его выполнения.
  2. Параллельная пометка (Concurrent Mark): сборщик мусора продолжает процесс пометки объектов, исследуя все связанные объекты, начиная с корневых объектов, и помечая их как достижимые. Этот этап выполняется параллельно с работой приложения, не останавливая его.
  3. Вторичная пометка (Remark): сборщик мусора выполняет еще одну короткую фазу “Stop-The-World”, чтобы обработать объекты, которые были изменены во время параллельной пометки. На этом этапе сборщик мусора обновляет информацию о достижимых объектах, чтобы учесть изменения, произошедшие во время параллельной пометки.
  4. Параллельное удаление (Concurrent Sweep): после завершения процесса пометки сборщик мусора удаляет все непомеченные объекты из памяти, освобождая занимаемый ими ресурс. Этот этап также выполняется параллельно с работой приложения, не останавливая его.
  5. Сброс (Reset): на этом этапе сборщик мусора очищает информацию о помеченных объектах и возвращает себя в исходное состояние, чтобы быть готовым к следующему циклу сборки мусора.

G1 (Garbage-First)

Это алгоритм сборки мусора, разработанный для замены CMS GC и решения некоторых его недостатков. G1 GC предназначен для обработки больших объемов памяти с минимальными паузами и предсказуемым временем сборки мусора. Вот некоторые ключевые аспекты и детали работы G1 GC:

  1. Разделение кучи на регионы: G1 GC разбивает область кучи (heap) на регионы фиксированного размера. Регионы могут быть молодыми (young), состоящими из областей эдема (Eden) и выживания (Survivor), или старыми (old).
  2. Сборка мусора на основе регионов: G1 GC сосредотачивает усилия на тех регионах, где больше всего мусора. Он идентифицирует регионы с наибольшим количеством мусора и выбирает их для очистки в первую очередь. Это позволяет оптимизировать процесс сборки мусора и уменьшить паузы.
  3. Параллельная и конкурентная сборка мусора: G1 GC использует множество потоков для выполнения задач сборки мусора. Большая часть работы выполняется параллельно с приложением, что уменьшает паузы, связанные с сборкой мусора. Однако, некоторые фазы G1 GC могут приостанавливать выполнение приложения (stop-the-world паузы), хотя эти паузы обычно короче, чем в других алгоритмах сборки мусора.
  4. Предсказуемость времени сборки мусора: G1 GC позволяет задать целевое время паузы для сборки мусора с помощью параметра -XX:MaxGCPauseMillis . Это позволяет гарантировать, что паузы, вызванные сборкой мусора, не превысят заданный порог, что полезно для приложений с требованиями к производительности и низким временем ожидания.
  5. Сжатие кучи: G1 GC также может выполнять сжатие кучи (heap compaction) для освобождения пространства и уменьшения фрагментации памяти. Он перемещает живые объекты, освобождая пространство для новых объектов и уплотняя области памяти. Это обеспечивает более эффективное использование памяти и сокращает время, необходимое для поиска свободного пространства при выделении новых объектов.
  6. Адаптивная настройка: G1 GC адаптируется к характеристикам приложения и системы, настраивая параметры сборки мусора во время выполнения. Он анализирует информацию о предыдущих циклах сборки мусора, такие как время сборки мусора, объем очищенной памяти и скорость выделения памяти, чтобы определить оптимальные параметры для последующих циклов сборки мусора.
  7. Журналирование и мониторинг: G1 GC предоставляет подробные сведения о своей работе через журнал событий сборки мусора и различные средства мониторинга, такие как Java VisualVM и JMX (Java Management Extensions). Это позволяет разработчикам и администраторам отслеживать процесс сборки мусора и оптимизировать параметры JVM для лучшей производительности приложения.

В целом, G1 GC является современным и эффективным алгоритмом сборки мусора, подходящим для больших приложений с требованиями к низкому времени ожидания и предсказуемым временем сборки мусора. Он обеспечивает улучшенную производительность и управление памятью по сравнению с другими алгоритмами сборки мусора, такими как Serial, Parallel и CMS GC.

JNI (Java Native Interface)

JNI (Java Native Interface) – это механизм, который позволяет взаимодействовать между Java-кодом и кодом, написанным на других языках программирования, таких как C, C++ или Assembly. JNI предоставляет набор функций и интерфейсов для вызова функций, написанных на других языках, из Java-кода и наоборот.

В JVM, JNI обеспечивает следующие возможности:

  1. Вызов функций из C/C++ из Java-кода: С помощью JNI, можно написать функции на C/C++, скомпилировать их в библиотеку и загрузить ее в JVM. Затем, можно вызывать функции из этой библиотеки из Java-кода, используя JNI-интерфейсы.
  2. Вызов функций Java из C/C++: JNI также позволяет вызывать функции, написанные на Java, из кода на C/C++. Для этого нужно получить доступ к JVM из кода на C/C++, создать объекты Java и вызывать методы, используя JNI-интерфейсы.
  3. Управление объектами Java из C/C++: JNI также позволяет создавать, изменять и управлять объектами Java из кода на C/C++. Для этого нужно получить доступ к JVM из кода на C/C++, создать объекты Java и использовать JNI-интерфейсы для их манипулирования.
  4. Управление памятью: При использовании JNI, нужно быть особенно внимательным при управлении памятью. Когда объекты создаются в коде на C/C++, они не управляются сборщиком мусора JVM. Поэтому, необходимо явно освобождать память, выделенную под объекты Java в коде на C/C++, используя JNI-интерфейсы.
  5. Безопасность: Использование JNI может быть опасным, так как это открывает доступ к низкоуровневому коду на C/C++. Для обеспечения безопасности, JVM имеет механизмы проверки безопасности, которые позволяют ограничивать доступ к ресурсам системы.
  6. Переносимость: JNI обеспечивает переносимость кода между различными платформами, так как код на C/C++ может быть скомпилирован для разных архитектур и операционных систем. Однако, необходимо учитывать различия в библиотеках и интерфейсах на разных платформах.

Хотя JNI не является одним из основных компонентов JVM, он играет важную роль в расширении возможностей JVM и интеграции Java с другими языками программирования. JNI позволяет использовать функции и библиотеки на C/C++ в Java-приложениях и вызывать функции Java из кода на C/C++.

Использование JNI может быть полезным при работе с низкоуровневым кодом, оборудованием и библиотеками, написанными на других языках программирования, что может улучшить производительность и расширить возможности Java-приложений.

На этом мы завершаем обзор основных компонентов JVM и принципов их работы.

Share this:

  • Click to share on Twitter (Opens in new window)
  • Click to share on Facebook (Opens in new window)

Архитектура виртуальной машины Java: объяснение для начинающих

Независимо от того, приходилось ли вам разрабатывать на Java, вы, вероятно, слышали о виртуальной машине Java (JVM).

JVM — ядро экосистемы Java. Она позволяет программам на базе Java следовать принципу “написал один раз, запустил где угодно”. Вы можете написать Java-код на одной машине и запустить его на любой другой благодаря JVM.

JVM изначально разрабатывалась исключительно для поддержки Java. Однако со временем на платформе Java обосновались многие другие языки, такие как Scala, Kotlin и Groovy. Все они в совокупности называются языками JVM.

В этой статье мы расскажем больше о JVM: как она работает и из каких компонентов состоит.

Что такое виртуальная машина?

Прежде чем переходить к JVM, остановимся на самой концепции виртуальной машины (ВМ).

Виртуальная машина — это виртуальное представление физического компьютера. Виртуальную машину можно назвать гостевой, а физический компьютер, на котором она работает, — хост-машиной.

На одной физической машине может работать несколько виртуальных, каждая со своей собственной операционной системой и приложениями. Эти виртуальные машины изолированы друг от друга.

Что такое виртуальная машина Java?

В языках программирования, таких как C и C++, код сначала компилируется в машинный для конкретной платформы. Эти языки называются компилируемыми языками.

С другой стороны, в таких языках, как JavaScript и Python, компьютер выполняет инструкции напрямую, без необходимости компиляции. Эти языки называются интерпретируемыми.

Java использует комбинацию обоих методов. Код Java сначала компилируется в байтовый код и генерирует файл класса ( .class ). Этот файл класса затем интерпретируется виртуальной машиной Java для базовой платформы. Один и тот же файл класса может выполняться на любой версии JVM, на любой платформе и операционной системе.

Подобно обычным виртуальным машинам, JVM создает изолированное пространство на хост-машине. Это пространство может использоваться для выполнения Java-программ независимо от платформы или операционной системы компьютера.

Архитектура виртуальной машины Java

JVM состоит из трех отдельных компонентов:

  • загрузчик классов;
  • область памяти/данных среды выполнения;
  • механизм выполнения.

Рассмотрим каждый из них более подробно.

Загрузчик классов

Когда вы компилируете исходный файл .java , он преобразуется в байт-код как файл .class . Когда вы вызываете этот класс у себя в программе, загрузчик классов загружает его в основную память.

Как правило, первым в память загружается класс, содержащий метод main() .

Процесс загрузки класса состоит из трех этапов: загрузка, связывание и инициализация.

Загрузка

Загрузка включает бинарную репрезентацию (байт-код) класса или интерфейса с определенным именем и создание на его основе исходного класса или интерфейса.

В Java доступны три встроенных загрузчика классов:

  • Загрузчик классов начальной загрузки(Bootstrap Class Loader) — корневой загрузчик классов. Это суперкласс загрузчика классов расширений, который загружает стандартные пакеты Java, такие как java.lang , java.net , java.util , java.io и так далее. Эти пакеты находятся внутри rt.jar и других основных библиотек, присутствующих в каталоге $JAVA_HOME/jre/lib .
  • Загрузчик классов расширений (Extension Class Loader) — подкласс загрузчика классов начальной загрузки и суперкласс загрузчика классов приложений. Он загружает расширения стандартных библиотек Java, которые присутствуют в каталоге $JAVA_HOME/jre/lib/ext .
  • Загрузчик классов приложений (Application Class Loader) — конечный загрузчик классов и подкласс загрузчика классов расширений. Он загружает файлы, которые находятся в пути к классам (classpath). По умолчанию путь к классу устанавливается как текущий каталог приложения. Путь к классу также можно изменить, добавив параметр командной строки -classpath или -cp .

JVM использует метод ClassLoader.loadClass() для загрузки класса в память. Он пытается загрузить класс на основе полного имени.

Если родительский загрузчик классов не может найти класс, он делегирует работу дочернему загрузчику классов. Если последний загрузчик также не может загрузить класс, он создает исключение NoClassDefFoundError или ClassNotFoundException .

Связывание

После загрузки класса в память происходит процесс связывания. Связывание класса или интерфейса предполагает объединение различных элементов и зависимостей программы.

Связывание включает следующие шаги.

  • Проверка. На этом этапе проверяется структурная корректность файла .class путем проверки его на соответствие набору ограничений и правил. Если проверка по какой-либо причине завершается неудачей, выбрасывается исключение VerifyException .

Например, если код был создан на Java 11, но выполняется в системе, где установлена Java 8, этап проверки завершится неудачно.

  • Подготовка. На этом этапе JVM выделяет память для статических полей класса или интерфейса и инициализирует их значениями по умолчанию.

Предположим, что вы объявили в классе следующую переменную:

private static final boolean enabled = true;

На этапе подготовки JVM выделяет память для переменной enabled и устанавливает ее значение в значение по умолчанию для логического значения, которое равно false .

  • Решение. На этом этапе символические ссылки заменяются прямыми, присутствующими в пуле констант времени выполнения.

Например, если у вас есть ссылки на другие классы или постоянные переменные, присутствующие в других классах, они разрешаются на этом этапе и заменяются их фактическими ссылками.

Инициализация

Инициализация включает выполнение метода инициализации класса или интерфейса (известного как ). Сюда может входить вызов конструктора класса, выполнение статического блока и присвоение значений всем статическим переменным. Это заключительный этап загрузки класса.

К примеру, ранее мы объявили следующее:

private static final boolean enabled = true;

На этапе подготовки переменной enabled было присвоено значение по умолчанию false . На этапе инициализации этой переменной присваивается ее фактическое значение true .

Примечание: JVM имеет многопоточный характер. Может случиться так, что несколько потоков одновременно пытаются инициализировать один и тот же класс. Это может привести к проблемам параллелизма. Чтобы гарантировать правильную работу программы в многопоточной среде, необходимо обеспечить потокобезопасность.

Область данных среды выполнения

В области данных среды выполнения есть пять компонентов:

Рассмотрим каждый из них в отдельности.

Область метода

Здесь хранятся все данные уровня класса, такие как пул констант времени выполнения, данные полей и методов, а также код методов и конструкторов.

Если памяти, доступной в области метода, недостаточно для запуска программы, JVM выдает ошибку OutOfMemoryError .

Например, предположим, что мы объявили следующий класс:

public class Employee  
private String name;
private int age;

public Employee(String name, int age)
this.name = name;
this.age = age;
>
>

В этом примере данные уровня поля, такие как name и age , а также сведения о конструкторе загружаются в область метода.

Область метода создается при запуске виртуальной машины, и на каждую виртуальную машину приходится только одна область метода.

Область кучи

Здесь хранятся все объекты и соответствующие им переменные экземпляра. Это область данных времени выполнения, из которой выделяется память для всех экземпляров классов и массивов.

Например, предположим, вы объявили следующий экземпляр:

Employee employee = new Employee();

В этом примере создается экземпляр класса Employee и загружается в область кучи.

Куча создается при запуске виртуальной машины, и на каждую виртуальную машину приходится только одна область кучи.

Примечание: поскольку области метода и кучи совместно используют одну и ту же память для нескольких потоков, данные, хранящиеся здесь, не потокобезопасны.

Область стека

Всякий раз, когда в JVM создается новый поток, одновременно создается отдельный стек среды выполнения. Все локальные переменные, вызовы методов и частичные результаты хранятся в области стека.

Если для обработки в потоке требуется больший размер стека, чем доступно, JVM выдает ошибку StackOverflowError .

Для каждого вызова метода в памяти стека делается одна запись, которая называется фреймом стека. Когда вызов метода завершен, фрейм стека уничтожается.

Фрейм стека разделен на три части.

  • Локальные переменные. Каждый фрейм содержит массив переменных, известных как его локальные переменные. Здесь хранятся все локальные переменные и их значения. Длина этого массива определяется во время компиляции.
  • Стек операндов. Каждый фрейм содержит стек последним-вошел-первым-вышел (last-in-first-out, LIFO), известный как стек операндов. Он действует как рабочая область среды выполнения для любых промежуточных операций. Максимальная глубина этого стека определяется во время компиляции.
  • Данные фрейма. Здесь хранятся все символы, соответствующие методу. Здесь также хранится информация о блоке catch на случай исключений.

К примеру, есть следующий код:

double calculateNormalisedScore(List answers)  
double score = getScore(answers);
return normalizeScore(score);
>

double normalizeScore(double score)
return (score – minScore) / (maxScore – minScore);
>

В этом примере кода переменные, такие как answers и score , помещаются в массив локальных переменных. Стек операндов содержит переменные и операторы, необходимые для выполнения математических операций вычитания и деления.

Примечание: поскольку область стека не является общей, она по своей сути потокобезопасна.

Регистры счетчика программ

JVM поддерживает многопоточность. Каждый поток имеет собственный регистр счетчика программ для хранения адреса выполняемой в данный момент инструкции JVM. Как только инструкция выполнена, регистр обновляется следующей инструкцией.

Стеки нативных методов

JVM содержит стеки, которые поддерживают нативные методы, то есть такие методы, которые написаны на языке, отличном от Java, например C или C++. Для каждого нового потока также выделяется отдельный стек нативных методов.

Система выполнения

Как только байт-код загружен в основную память, и подробная информация становится доступна в области данных среды выполнения, наступает следующий этап — запуск программы. Механизм выполнения делает это, выполняя код из каждого класса.

Однако перед выполнением программы байт-код необходимо преобразовать в инструкции машинного языка. В качестве механизма выполнения JVM может задействовать интерпретатор или JIT-компилятор.

Интерпретатор

Интерпретатор считывает и выполняет инструкции байт-кода строка за строкой. Из-за построчного выполнения интерпретатор работает сравнительно медленнее.

Еще один недостаток интерпретатора — при многократном вызове метода каждый раз требуется новая интерпретация.

JIT-компилятор

JIT-компилятор преодолевает недостаток интерпретатора. Механизм выполнения сначала использует интерпретатор для выполнения байт-кода, но когда он находит какой-то повторяющийся код, то задействует JIT-компилятор.

Затем JIT-компилятор компилирует весь байт-код и изменяет его на собственный машинный код. Этот собственный машинный код используется непосредственно для повторных вызовов методов, что повышает производительность системы.

JIT-компилятор содержит следующие компоненты.

  • Генератор промежуточного кода — генерирует промежуточный код.
  • Оптимизатор кода — оптимизирует промежуточный код для повышения производительности.
  • Генератор целевого кода — преобразует промежуточный код в собственный машинный код.
  • Профилировщик — находит горячие точки (код, который выполняется повторно).

Чтобы лучше понять разницу между интерпретатором и JIT-компилятором, предположим, что у вас есть следующий код:

int sum = 10;
for(int i = 0 ; i sum += i;
>
System.out.println(sum);

Интерпретатор будет извлекать из памяти значение sum для каждой итерации в цикле, добавлять к нему значение i и записывать обратно в память. Это дорогостоящая операция, потому что каждый раз при входе в цикл происходит обращение к памяти.

Однако JIT-компилятор распознает, что в этом коде есть “горячая точка”, и выполнит оптимизацию. Он сохранит локальную копию sum в регистре для потока и будет продолжать добавлять значение i в цикле. Как только цикл завершится, компилятор запишет значение sum обратно в память.

Примечание: компилятору требуется больше времени для компиляции кода, чем интерпретатору для интерпретации кода строка за строкой. Если вы намерены запустить программу только один раз, интерпретатор будет предпочтительнее.

Сборщик мусора

Сборщик мусора (Garbage Collector, GC) собирает и удаляет объекты без ссылок из области кучи. Это процесс автоматического восстановления неиспользуемой памяти во время выполнения путем уничтожения мусорных объектов.

Сборка мусора делает память Java эффективной, потому что удаляет объекты без ссылок из памяти кучи и освобождает место для новых объектов. Она включает два этапа:

  • Пометка — на этом этапе GC идентифицирует неиспользуемые объекты в памяти.
  • Очистка — на этом этапе GC удаляет объекты, идентифицированные на предыдущем этапе.

Сборка мусора выполняется JVM автоматически через регулярные промежутки времени и не требует отдельной обработки. Ее также можно запустить вызовом System.gc() , но выполнение не гарантируется.

JVM содержит три различных типа сборщиков мусора.

  • Последовательная сборка мусора. Это самая простая реализация GC. Она предназначена для небольших приложений, работающих в однопоточных средах. Для сборки мусора используется один поток. Запуск приводит к событию “остановки мира”, когда все приложение приостанавливает работу. Аргумент JVM для запуск последовательного сборщика мусора: -XX:+UseSerialGC .
  • Параллельная сборка мусора. Это реализация GC по умолчанию, также известная как сборщик пропускной способности. Для сборки мусора в нем используется несколько потоков, но работа приложения все равно приостанавливается при запуске. Аргумент JVM для параллельного сборщика мусора: -XX:+UseParallelGC .
  • Garbage First (G1). G1 был разработан для многопоточных приложений с большим доступным размером кучи (более 4 ГБ). Он разбивает кучу на набор областей одинакового размера и использует несколько потоков для их сканирования. G1-сборщик определяет регионы с наибольшим количеством мусора и сначала выполняет сбор мусора в них. Аргумент JVM для этого сборщика мусора: -XX:+UseG1GC .

Примечание: существует другой тип сборщика мусора, называемый сборщиком параллельных меток (CMS). Однако он устарел начиная с Java 9 и полностью удален в Java 14, и его место занимает сборщик G1.

Нативный интерфейс Java (Java Native Interface, JNI)

Иногда необходимо задействовать в работе нативный (не Java) код (например, написанный на C/C++). К примеру, в тех случаях, когда нужно взаимодействовать с физическим оборудованием или преодолевать ограничения по управлению памятью и производительности в Java. Java поддерживает выполнение нативного кода через нативный интерфейс Java (JNI).

JNI действует как мост для предоставления вспомогательных пакетов другим языкам программирования, таким как C, C++ и так далее. Это особенно полезно в тех случаях, когда нужно написать код, который не полностью поддерживается Java, например, некоторые специфичные для платформы функции могут быть написаны только на C.

Вы можете воспользоваться ключевым словом native , чтобы указать, что реализация метода будет предоставлена нативной библиотекой. Также потребуется вызвать System.LoadLibrary() , чтобы загрузить общую нативную библиотеку в память и сделать ее функции доступными для Java.

Нативные библиотеки методов

Нативные библиотеки методов — это библиотеки, написанные на других языках программирования, таких как C, C++ и ассемблер. Эти библиотеки обычно представлены в виде файлов .dll или .so . Такие библиотеки можно загружать через JNI.

Распространенные ошибки JVM

  • ClassNotFoundException . Происходит, когда загрузчик классов пытается загрузить классы с помощью Class.forName() , ClassLoader.loadClass() или ClassLoader.findsystemclass() , но определение класса с указанным именем не найдено.
  • NoClassDefFoundError . Происходит, когда компилятор успешно скомпилировал класс, но загрузчик классов не может найти файл класса во время выполнения.
  • OutOfMemoryError . Происходит, когда JVM не может выделить объект из-за нехватки памяти, и сборщик мусора не может предоставить больше памяти.
  • StackOverflowError . Происходит, если в JVM не хватает места при создании новых кадров стека во время обработки потока.

Заключение

В этой статье мы обсудили архитектуру виртуальной машины Java и ее компоненты. Часто мы не вникаем глубоко во внутреннюю механику JVM или не интересуемся, как она работает, пока работает код.

Только когда что-то идет не так, и нам нужно настроить JVM или устранить утечку памяти, мы пытаемся понять ее внутреннее устройство.

Это также очень популярный вопрос во время интервью на роль бэкенд-разработчика — как младшего, так и старшего уровня. Глубокое понимание JVM поможет вам лучше писать код и избегать подводных камней, связанных со стеком и ошибками памяти.

  • 9 вещей, которыми следует заняться Java программисту в 2018 году
  • Превратите свой Java-код в полностью асинхронный
  • Циклы Java в сторону — даешь потоки!

Как устроена Java Virtual Machine и её экосистема: подробный гайд

Иван Углянский рассказал, как устроена JVM, что с ней не так и зачем Python, Ruby и другие языки переезжают на виртуальную машину Java.

Иллюстрация: Mark Kingston / WikiMedia Commons / Colowgee для Skillbox Media

Антон Сёмин

Антон Сёмин

Пишет об истории IT, разработке и советской кибернетике. Знает Python, JavaScript и немного C++, но предпочитает писать на русском.

об эксперте

JVM engineer. Работает в Excelsior@Huawei над виртуальными машинами Java, компиляторами и новыми языками программирования. Один из основателей и лидеров JUGNsk, член программного комитета SnowOne Conference.

ссылки

Я занимаюсь JVM более 10 лет. Начинал в новосибирской компании Excelsior, которая разрабатывала свою реализацию виртуальной машины Java — Excelsior JET. Потом мы вошли в состав Huawei, но команда осталась прежней.

Что такое JVM и как она спасла программистов от профессионального «подгорания»

До появления Java, примерно 25 лет назад, многие приложения писали на С или С++. У этих языков есть проблема: разработчику приходится думать, на какой операционке и архитектуре процессора будет работать его код. Например, если он пишет программу под Linux, то, скорее всего, она не запустится на Windows или MacOS. Поэтому код приходилось пичкать директивами условной компиляции или писать отдельную версию для каждой операционки.

Самую сильную боль вызывали приложения с GUI — код графических компонентов для разных операционок был совершенно разным. Причина в том, что C и C++ довольно близки к железу, а значит, сишник должен учитывать архитектуру процессора и тип операционной системы. Понятно, что никакой кроссплатформенностью здесь и не пахнет.

Ещё в 1960-е годы у инженеров появилась идея: писать программы не для конкретного железа, а для абстрактного «исполнителя». Программы на Java как раз пишутся для такого исполнителя — виртуальной машины, или Java Virtual Machine (JVM). Java-разработчик не задумывается, на какой платформе будет запускаться его код. В то же время виртуальная машина не знает, что исполняет инструкции на Java, ведь она принимает и исполняет байт-код.

В этом и было «уникальное торговое предложение» Java: разработчики писали программу под Windows, а она запускалась на macOS почти без изменений. Сейчас это звучит вполне естественно, а 25 лет назад казалось настоящим чудом.

Кроссплатформенность в Java обеспечивается явным разделением уровней языка и реализации.

Языковой уровень. Разработчики пишут код на языке Java, синтаксис и семантика которого описаны в Java Language Specification. После этого специальным инструментом, который называется javac, исходный код компилируется в байт-код Java. При этом происходит проверка синтаксиса, и в случае его нарушения разработчик получает сообщение об ошибке от javac.

Что здесь важно:

  • На этом этапе нет ничего платформенно-специфичного, весь код на языке Java (как и байт-код Java) универсален.
  • Байт-код Java — это язык, предназначенный не для людей, а для машин. Обычному разработчику его читать не нужно.

Уровень реализации. Полученный байт-код Java передаётся на вход виртуальной машины Java. И вот как именно она будет его исполнять, описано уже в другой спецификации — Java Virtual Machine Specification. Со всеми особенностями конкретной операционной системы или архитектуры процессора тоже разбирается JVM, без влияния на исходный код на языке Java. Таким образом, происходит перенос ответственности: разработчики на Java о таких неприятных вещах больше не думают, им достаточно просто взять правильную JVM (например, для Linux x64). А все OS/arch-специфические нюансы решают разработчики JVM.

Язык Java и сама JVM разрабатываются параллельно, при этом новые фичи языка зачастую требуют поддержки внутри JVM. С другой стороны, различные реализации JVM развиваются и сами по себе, например в них появляются новые, более эффективные алгоритмы сборки мусора или оптимизации кода.

У разделения языка и его реализации есть замечательное следствие: разработчику не обязательно ограничиваться языком Java, ведь виртуальная машина понятия не имеет, откуда взяли байт-код, который пришёл к ней на вход, был ли это изначально Java-код или что-то другое. То есть программу можно писать на любом языке, который транслируется в байт-код. И таких языков — целое семейство: Kotlin, Clojure, Groovy и так далее. Например, программы на Kotlin компилируются с помощью kotlinc в class-файлы, а затем подаются на вход JVM.

Как соотносятся JDK и Open JDK

Java Development Kit (JDK) — это комплект инструментов для разработки приложений на Java. В него входят несколько компонентов, которые позволяют разрабатывать и запускать приложения:

  • Javас. Это компилятор, который преобразует исходники Java в class-файлы или формат jar.
  • JVM. Виртуальная машина, которая исполняет байт-код.
  • Стандартная библиотека. Набор модулей, классов, объектов и функций, которые доступны в языке.
  • Документация. В ней содержится справочная информация об инструментах данной версии JDK.

Когда появляется новая версия языка, все эти компоненты обновляются.

В случае стандартной JVM (например, HotSpot) запуск Java-приложения выглядит так: сначала с помощью javac вы компилируете исходный код в class-файлы (или jar-файлы), и именно их вы можете поставлять в качестве приложения.

Чтобы приложение запустилось на хосте, там должна быть установлена виртуальная машина. Тогда пользователь пишет в командной строке: java -jar имя вашего jar-файла> — и программа запускается.

С нашей виртуальной машиной сценарий запуска приложения был другой: мы заранее компилировали class-файлы в один исполняемый exe-файл. Поэтому он работал на компьютере пользователя без каких-либо предустановленных JVM. Но, по сути, JVM никуда не исчезла — просто все её части были слинкованы в один экзешник.

В спецификации Java Virtual Machine почти ничего не сказано о том, как именно её реализовывать — только что должно получиться на выходе. Например, вы не можете явно освободить память из под объекта, когда он вам уже не нужен. Этим занимается специальный компонент JVM — Garbage Collector. Но вот как именно он работает, в спецификации не сказано.

Так и было задумано: спецификация позволяет создавать разные JVM, реализации которых в чём-то отличаются, но при этом удовлетворяют требованиям спецификации. И это замечательно, пусть расцветают сто цветов! Такие различия в реализации дают разным виртуальным машинам особые конкурентные преимущества: у кого-то более крутой сборщик мусора, у кого-то быстрый стартап, а кто-то реализовал более мощные оптимизации в компиляторе.

Первую JVM, как и язык Java, разработали в компании Sun Microsystems. Когда Sun опубликовала спецификацию, Oracle стала работать над своей JRockit, а IBM — над OpenJ9. Наша Excelsior JET вышла в свет в 2000 году.

Изначально все JVM были закрытыми, но в 2006 году Sun опубликовала исходники своей JVM HotSpot и компилятора javac, а потом и стандартной библиотеки Java. Это дало старт проекту OpenJDK — полностью открытой реализации JDK. Через какое-то время Sun была поглощена корпорацией Oracle, но при этом проект OpenJDK никуда не делся и до сих пор остаётся главной open-source-площадкой для разработки Java и JVM.

OpenJDK — это эталонная реализация, на которую ориентируются разработчики других виртуальных машин. Проект распространяется под GPL 2 + Classpath Exception, благодаря чему любая компания или разработчик, которым хватит квалификации, смогут форкнуть OpenJDK и начать разрабатывать свою собственную JVM. Конечно же, по условию лицензии их реализация тоже будет open source.

Существуют и реализации JVM, которые никак не связаны с OpenJDK. Ведь не только Sun Microsystems и Oracle создавали JVM. Другой пример — это IBM J9, которая, кстати, тоже открыла исходный код и стала называться OpenJ9.

Некоторые компании продают свои виртуальные машины. Например, у Azul есть свободная сборка Zulu и закрытая коммерческая Azul Zing. В последней есть уникальный сборщик мусора C4, который гарантировал низкие паузы ещё до того, как это стало мейнстримом. А также технология ReadyNow!, которая позволяет «запомнить» список всех оптимизаций кода и «проиграть» его на старте JVM, выводя машину на пиковую производительность.

Excelsior JET тоже была закрытой и платной. Она обеспечивала быстрый стартап, низкое потребление памяти и хорошую пиковую производительность во многих сценариях. Всё это мы получали за счёт статической компиляции, в отличие от динамической компиляции на лету в HotSpot.

Однако сегодня люди чаще пользуются бесплатными сборками, основанными на OpenJDK. Существуют как стандартные сборки OpenJDK от Oracle, так и сборки от других компаний и разработчиков, имеющих отношение к разработке OpenJDK (зачастую их публикуют контрибьюторы в OpenJDK не из Oracle).

В таких сборках могут быть дополнительные фичи, не включённые по умолчанию в стандартные сборки от Oracle. Либо такие альтернативные сборки прошли дополнительную сертификацию, например для использования с «КриптоПро». При этом такие сторонние билды проходят все стандартные тесты, поэтому гарантируется, что ваши программы будут работать как надо и пользоваться ими можно абсолютно безопасно и бесплатно.

Некоторые компании не только выкладывают свои билды, но и оказывают платную поддержку пользователям. Допустим, возникла проблема с виртуальной машиной: ваше приложение работает неожиданно медленно или даже крашится! Всякое бывает, ведь JVM — это тоже программа, в которой могут быть свои баги. Тогда инженеры этой компании оперативно разберутся в проблеме и решат её.

В целом вариантов виртуальных машин и их сборок очень много — в рамках этой статьи все не перечислить. Есть сборки от Oracle, Red Hat, Azul, IBM, BellSoft, Amazon и Microsoft. Сообщество Java-чемпионов написало замечательный документ, в котором перечислило все варианты и в целом разъяснило ситуацию с многообразием JVM и их сборок. Документ называется Java Is Still Free, рекомендую его всем к прочтению.

Как и зачем другие языки переходят на JVM

Java — это не просто язык, а целая философия. И она оказалась настолько востребованной, что у других языков программирования стали появляться реализации под JVM. Например, Jython и JRuby.

Главная причина появления таких реализаций — перформанс. За 25 лет человечество научилось делать хорошо оптимизированные JVM. Так почему бы за счёт этого не повысить производительность программ на Python и Ruby?

С другой стороны, так можно получить хорошую связь с экосистемой Java. Если мы запускаем код на JVM, он лучше взаимодействует с другими модулями на JVM. Значит, можно пользоваться и Java-библиотеками.

У Kotlin немного другая история. Когда язык только появился, было решено компилировать его в байт-код Java и запускать на JVM. Таким образом, реализация языка заключалась в написании хорошего транслятора — kotlinc — из исходного кода .kt в class-файлы. А реализация низкоуровневых компонентов типа сборщиков мусора или взаимодействия с операционной системой делегировалась уже существующим JVM.

Относительно недавно создатели Kotlin сделали Kotlin Native — технологию компиляции Kotlin в нативный код напрямую, без JVM. В нём низкоуровневые компоненты, такие как менеджер памяти, уже реализованы самостоятельно.

Хотя на словах кажется, что любой язык программирования легко перевести на JVM (достаточно написать транслятор в class-файлы), на деле всё оказывается намного сложнее. Грамотно отобразить фичи языка в байт-код Java и при этом получить хорошую производительность — нетривиальная задача. Посмотрите хотя бы на JRuby.

Чарльз Наттер часто делится опытом поддержки различных фич Ruby в проекте JRuby и рассказывает о сложностях, с которыми сталкивается. Чем дальше исходный язык от Java, тем сложнее задача трансляции. Но, думаю, всё возможно, и JRuby — наглядный тому пример.

Какие конкуренты есть у JVM

Самый известный конкурент Java Virtual Machine — платформа .NET и их виртуальная машина для реализации C#.

В начале 2000-х Microsoft делала свою виртуальную машину Java — Microsoft J++. Но из-за того, что корпорация не соблюдала спецификацию, Sun подала на неё судебный иск. Microsoft проиграла все суды и лишилась права делать виртуальную машину для Java. Есть мнение, что это стало одной из причин появления C#.

Java и C# решают одни и те же задачи и обладают одними и теми же преимуществами: строгая типизация, сборка мусора, безопасность — всё это совпадает. Это managed-языки широкого применения.

Раньше между платформами была одна принципиальная разница: Java был кроссплатформенным, а .NET и C# работали только на Windows. Но с тех пор, как появился .NET Core, C# тоже стал мультиплатформенным.

Отмечу, что сами языки Java и C# довольно разные, и C# развивается быстрее: там регулярно появляются интересные фичи, которые в Java приходят с большим опозданием или не приходят вообще. Поэтому как язык C# в целом выглядит интереснее.

Зато у Java мощная реализация и хорошо оптимизированные виртуальные машины. Это стало возможным благодаря «гонке вооружений» между разработчиками виртуальных машин. Вендоры на протяжении 20 лет соревновались, кто сделает самую крутую оптимизацию, лучший алгоритм сборки мусора, более быстрый стартап и так далее. Поэтому по многим показателям реализация Java выглядит куда лучше C#. Некоторые вещи, которые уже давно есть в JVM, только-только появляются в .NET.

Есть и другие языки, которые претендуют на роль конкурента Java, но они отличаются гораздо сильнее. Например, Python, который недавно занял 1-е место в рейтинге TIOBE. Он популярен в Data Science, AI, машинном обучении, скриптах и так далее. К сожалению, динамическая типизация не позволяет держать на нём слишком большую кодовую базу.

Ещё есть безумно популярный JavaScript, но его интересы с Java не очень пересекаются, так как сегодня Java во фронтенде не используется. Хотя когда-то были и такие планы (привет устаревшей технологии Java-апплетов).

Где Java хорош, а где — нет

Java широко используют в бэкенде, то есть во всех приложениях, которые работают на стороне сервера. Его главный конкурент — C#, но Java всё-таки значительно лидирует, потому что считается более кроссплатформенным. Хотя последнее уже не совсем правда. Также стоит отметить, что у Java банально есть фора по времени по сравнению с C#. Он появился на 10 лет раньше, поэтому успел захватить умы разработчиков и кодовые базы.

Почему Java, а не «швейцарский нож» С++? Просто Java удобнее и безопаснее для жизни, или, как принято говорить, не даст выстрелить себе в ногу. Разработчику не нужно думать об управлении памятью и сегфолтах, которые могу прилететь хоть откуда. Поэтому, когда дело касается будничных задач бэкенда, скорость разработки на Java и C# гораздо выше, чем на C++.

Java плохо подходит для приложений, которым требуется высокая производительность и гарантированное время отклика. Тем не менее некоторые пишут на Java и такие программы, если готовы мириться с небольшими задержками в микросекунды. Для них выигрыш от скорости разработки и надёжности важнее. Но писать на Java программное обеспечение для кардиостимуляторов, где все элементы должны работать точно, как швейцарские часы, — не стоит.

С другой стороны, приложения для трейдинга всё чаще пишут именно на Java. Казалось бы, где, если не в трейдинге, важен быстрый отклик — чтобы не потерять миллионы из-за скачка цен на акции Tesla? Однако на практике надёжность здесь важнее скорости: лучше торговать медленнее, но спокойно и надёжно, чем написать на С++ быструю биржу, которая через пять минут торгов крашнется. Java гарантирует безопасность, и это круто.

Что не так с JVM

В истории развития любого языка программирования всегда находятся тонкие места или сомнительные решения. Java здесь не исключение, встречаются такие спорные вопросы и в спецификациях Java и JVM.

Система модулей

Одна из спорных тем — Project Jigsaw и система модулей, появившаяся в девятой версии Java. Создатели Java долго делали систему, которая позволяет разбивать Java-программу на отдельные логические составляющие — модули. Это дополнительный уровень абстракции над давно существующими packages в Java.

На неё потратили много времени — ведь нужно было организовать поддержку в самом языке, виртуальной машине и других частях JDK. Система долго не попадала в релизную версию, а когда всё-таки попала, оказалась, мягко говоря, не слишком востребованной пользователями.

Как в языке появляются новые фичи и спецификации? Есть специальный совет Java Community Process (JCP), который рассматривает предложения Java Specification Request (JSR) и голосует за или против. В этом совете есть представители крупных компаний, есть представители user group и даже отдельные люди — в общем, представлены интересы разных сторон Java Community.

Идея новой модульной системы в Java была несколько раз отвергнута JCP (что вообще случается довольно редко), но всё-таки вошла в язык после значительных доработок и послаблений. Тем не менее спустя несколько лет мы видим, что идея так и не пришлась по вкусу Java-разработчикам и модулями пользуются редко.

Дженерики

Дженерики в Java, в отличие от многих других языков, — стираемые. Это означает, что все типовые параметры стираются через javac до Object. В рантайме вы не сможете понять, какой тип вам на самом деле пришёл. У этого подхода есть свои плюсы и минусы, а споры о том, что лучше — стираемые или нестираемые дженерики — продолжаются и по сей день. Тем не менее, выбор в пользу стираемых дженериков в Java сделан не случайно. Подробное обоснование, почему дженерики в Java такие, можно почитать вот в этой статье Брайана Гетца «Background: how we got the generics we have».

Но как бы вы не относились к реализации дженериков в Java, сейчас есть существенная проблема: нельзя использовать примитивные типы в качестве типовых параметров (ведь они несовместимы с Object). Это сокращает выразительную силу языка и вынуждает либо использовать Boxed-типы (что может ухудшить производительность), либо и вовсе специализировать функции под примитивные типы вручную. Исправить это собираются с помощью специализированных дженериков в рамках проекта Valhalla.

Обратная совместимость

Обратная совместимость — один из главных принципов Java, которым совет JCP не пожертвует даже ради самой передовой фичи. От некоторых вещей иногда отказываются, но при этом старый код всё равно должен продолжать работать.

Например, реализация исключений через инструкции JSR/RET встречается в старом байт-коде, а в новом — нет. Но до сих пор во всех тулах нужно уметь поддерживать старый байт-код ради библиотеки, написанной 20 лет назад, но которая так и продолжает использоваться в современных проектах в виде того же старого формата .jar.

С одной стороны, этот принцип позволяет построить стабильную экосистему. Бизнес может не бояться, что выйдет новая версия, которая всё сломает и навсегда заблокирует путь к обновлению. С другой стороны, это тормозит инновации. Возможно, Java развивается медленнее, чем C#, потому, что у нас обратная совместимость — это священная корова, которую нельзя трогать.

Насколько глубоко разработчику нужно знать JVM

На самом деле это очень холиварный вопрос. В основном ответ на него зависит от задач, которые хочет решать джавист.

Думаю, 30% программистов могут спокойно работать и не понимать, что происходит внутри виртуальной машины. Другим 50% достаточно на среднем уровне знать, как устроена JVM, — тогда они будут отличными мидлами и сеньорами. Их работа не в том, чтобы копаться в кишках виртуальной машины, но некоторые знания о её устройстве помогут им писать более эффективный код и избежать многих ошибок. В оставшихся 20% задач без знания деталей всё-таки не обойтись. Получается, чем лучше вы разбираетесь в виртуальной машине, тем более широкий класс задач способны решать.

Если в проекте важна производительность и повысить её с помощью выбора более оптимального алгоритма нельзя, надо разбираться, как работает сборщик мусора и как повысить его эффективность, какие настройки можно поменять, что такое деоптимизация, почему она происходит и так далее.

Java-разработчик, который регулярно погружается в эту тему, со временем сможет стать Performance Engineer. Эти специалисты находят проблемы производительности, анализируют глубинные процессы в JVM и исправляют их. Они решают проблемы не дописыванием кода в виртуальную машину, а настраивают её, как часовщики.

С точки зрения культуры всем разработчикам на managed-языках полезно знать, что происходит под капотом машины, когда они нажимают кнопку Run в IDE. Конечно, не нужно запоминать, что и в какой момент делает каждая деталь JVM. Но пройти вводный курс и понять на концептуальном уровне, что такое интерпретатор, Just In Time Compiler, что значит «умер объект» и так далее, — полезно.

Разработчик, который не знает ответов на все эти вопросы, способен решить довольно много задач. Но однажды он столкнётся с ошибками, причины которых даже не сможет понять. Например, если не задумываться, откуда берётся память и как она возвращается операционной системе, то можно получить от IDE сообщение Out of memory. А всё потому, что программа потребляет слишком много памяти. Разработчик, который знает, как устроена память, постарается оптимизировать код и не создавать горы объектов.

Куда развивается JVM

Сейчас довольно активно развиваются три проекта, которые рано или поздно войдут в новые версии Java.

Loom

Loom — это проект по добавлению виртуальных тредов в Java. С ним треды не мапятся на треды операционной системы, а живут сами по себе. И лишь когда их нужно исполнить, они будут садиться на треды операционки.

Раньше каждому экземпляру класса java.lang.Thread (через которые реализуется многопоточное исполнение) соответствовал один поток операционной системы. Вполне логичное решение, но есть проблема: потоков операционной системы не слишком много, поэтому вряд ли у вас получится создать больше нескольких тысяч потоков, реализованных таким образом. Project Loom предлагает альтернативу — виртуальные потоки, которые уже не связаны соотношением 1 к 1 с потоками виртуальной системы. Вместо этого они время от времени садятся на эти OS threads, чтобы выполнить очередной квант работы.

Идея не новая. Например, в Go так было всегда. Да и вообще о виртуальных тредах говорили уже много десятилетий. И даже в самом Java в 1990-е были грин-треды — схожая идея, где N зелёных потоков назначались на один поток операционной системы (тогда в процессорах было по одному ядру и проблема легковесной многопоточности не стояла так остро). Можно сказать, что Loom — это в некотором смысле реинкарнация зелёных тредов, но сделанная на более серьёзном уровне.

Этот проект очень важен, так как в современном мире микросервисов и серверов с огромным количеством одновременных подключений вам часто хотелось бы создавать десятки и сотни тысяч тредов. Со старым подходом к реализации через OS threads это просто невозможно, а вот с Loom всё получится.

Panama

Panama — это проект, посвящённый взаимодействию между Java и нативным кодом, написанным, например, на C или C++. Конечно, Java мог взаимодействовать с нативным кодом с самого рождения. Основное решение в этой области — Java Native Interface, очень мощный, но медленный и неудобный в использовании механизм. Чтобы взаимодействовать с нативами, приходилось писать некрасивый, громоздкий и, что самое главное, небезопасный код. Я уверен, что любому джависту, который хоть что-нибудь делал с JNI, как минимум не понравилось, а как максимум — он до сих видит кошмары про развалы в JNI.

Чтобы избавить Java-разработчиков от ночных кошмаров, авторы Panama уже много лет работают над своим проектом, который, как Панамский канал, соединяет два мира: нативный и Java. Panama предлагает новый интерфейс и его оптимизированную реализацию на стороне JVM, которая позволит приятно и безопасно работать с нативным кодом, в том числе с off-heap .

Valhalla

Проект Valhalla посвящён введению в Java новых типов объектов, так называемых value-типов, или inline-типов, или же primitive-value-типов — это текущее название. Да, проект настолько сложный, что даже основная его концепция часто меняет имя!

Если коротко, то смысл в том, чтобы выделить подкласс объектов, у которых нет identity. Что это такое? В Java вы можете создать два абсолютно одинаковых объекта с помощью оператора new. Потом сравнить их на == и получить false. Это разные объекты! Они сравниваются не по содержимому (которое одинаковое), а по некоторому уникальному свойству, назначаемому каждому объекту при создании. Вот возможность сравнивать не по содержимому, а по чему-то ещё — это и есть identity.

С другой стороны, можно объявить две переменные типа int, записать в каждую из них значение 42, сравнить на == и получить результат true. Всё потому, что у int никакого identity нет, они сравниваются по содержимому.

Цель Valhalla — дать пользователям возможность самим описывать классы, у которых нет identity, и подготовить к этому уже существующую типовую систему. Как следствие, можно будет, например, заводить плоские поля таких типов внутри других классов (то есть фактически инлайнить их поля внутрь других объектов), работать с плоскими массивами таких объектов и даже получить специализированные дженерики.

Это огромное, фундаментальное изменение всей платформы Java, за которым очень интересно следить и которого стоит ожидать, ведь оно принесёт множество возможностей обычным разработчикам.

На каких языках разрабатывают JVM и библиотеки для Java

Сами виртуальные машины Java обычно пишут на С++, стандартная библиотека же в основном написана на самом Java (хотя иногда и приходится уходить в нативный код, то есть снова в плюсы). Вообще, так повелось, что системщину пишут на плюсах, ведь здесь, с одной стороны, важна производительность, а с другой — связь с низким уровнем, с операционной системой и железом. В то же время стало очевидно, что далеко не все компоненты виртуальной машины обязательно нужно писать на С++. Например, зачем делать компилятор на С++, если есть замечательные managed-языки вроде Java или Scala?

Вот, например, в Oracle Labs разрабатывает проект GraalVM. Он состоит из трёх компонентов:

  • КомпиляторGraal — мощный оптимизирующий компилятор, в будущем он, возможно, заменит компилятор C2 из HotSpot. Он не уступает по качеству многих оптимизаций, но написан полностью на Java.
  • Truffle Framework — специальный фреймворк для написания интерпретаторов других языков, которые потом запускаются на GraalVM. По сути, это быстрый способ реализовать любой язык программирования на базе GraalVM! Опять-таки, интерпретаторы пишутся на managed-языках.
  • Graal Native Image — отдельный режим сборки приложений, который на самом деле представляет из себя целую виртуальную машину, но написанную с некоторыми нарушениями спецификации. Зато она позволяет получить быстрый стартап и низкое потребление памяти. Рантайм для такой виртуальной машины тоже частично написан на Java! Понятно, что не полностью и с разными ограничениями языка (на самом деле там используется отдельный диалект — System Java), но тем не менее это возможно.

Вообще, писать виртуальные машины на managed-языках — давняя мечта системщиков. Ведь, например, компилятор С написан на С? Вот и разработчики виртуальных машин для managed-языков хотят писать их на Java или других managed-языках, а не на опасном и болезненном С++.

Как контрибьютить в OpenJDK

Для начала скажу, что я никогда не контрибьютил в OpenJDK, а занимался разработкой другой, проприетарной и закрытой JVM. Поэтому о процедуре коммита в OpenJDK имею только смутное представление, так что написанному ниже доверяйте на свой страх и риск!

Если бы я задался целью контрибьютить в OpenJDK, то думал бы в двух направлениях:

  • Найти что-то небольшое, что получится исправить или улучшить. Это может быть несложный (и низкоприоритетный) баг в баг-трекере OpenJDK или неэффективность какого-нибудь алгоритма в платформенных классах. Исправьте эту проблему, соберите OpenJDK и проверьте, что, например, алгоритм и правда стал работать лучше на каких-нибудь бенчмарках.

Потом напишите в мейлинг-лист, расскажите о сути улучшения и попросите сделать ревью. Наконец, пройдите его, а затем и остальную процедуру коммита. Насколько я знаю, она довольно стандартная, ведь OpenJDK теперь находится на GitHub. Поздравляю, теперь вы контрибьютор в OpenJDK!

По рассказам людей, которые прошли по этому пути, вся процедура может занять довольно много времени, особенно в первый раз, но в целом это вполне реально.

  • Устроиться на работу в компанию-контрибьютор. В этом случае вы будете разрабатывать код, который попадёт в OpenJDK, в рабочее время. Однако сложность в том, что таких компаний всё-таки очень мало. Зато попасть в одну из них — действительно круто. Яркий пример такой компании — питерская BellSoft, которая в прошлом году заняла четвёртое место в мире (!) среди внешних (не из Oracle) контрибьюторов в OpenJDK.

На кого подписаться и где следить за новостями JVM

Разработчики виртуальных машин регулярно улучшают свои VM и зачастую делятся новостями о разработке и состоянии тех или иных проектов в рамках OpenJDK и не только. Черпать актуальную информацию можно из мейлинг-листов, конференций и Twitter.

Мейлинг-листы

Мейлинг-лист — это такой почтовый ящик, на который можно подписаться. Подписчики мейлинг-листа получают письма из общей рассылки, но ещё могут отвечать, участвовать в дискуссиях или задавать вопросы. Именно так и общаются разработчики многих языков программирования.

Мейлинг-листов про Java и JVM довольно много, есть свои мейлинг-листы для разных компонентов JDK и разных подпроектов в OpenJDK. Например, есть отдельные мейлинг-листы для упомянутых выше проектов Loom или Valhalla. Так что тут главное — выбрать темы, которые вам интересны, а потом найти соответствующие мейлинг-листы на сайте сообщества Java.

Новые предложения по технологии в первую очередь пишут и обсуждают именно в мейлинг-листах, поэтому в них отражается история развития Java и JVM. Такой канал связи действительно работает, хоть и может показаться немного устаревшим. Если хотите узнать сложные детали, например как работает конкретная часть виртуальной машины, советую подписаться на соответствующий мейлинг-лист.

Конференции

Более привычный и простой для восприятия источник информации — конференции. Разработчики JVM регулярно выступают с докладами, в которых по косточкам разбирают детали реализации, при этом в доступной и понятной форме.

Наибольшая концентрация знаний — на ежегодной конференции Java Virtual Machine Language Summit. Видео выступлений можно посмотреть на YouTube-канале Java. Очень много хардкорных и качественных докладов от разработчиков JVM есть на российских конференциях Joker и JPoint от компании JUG Ru Group.

Сами мы (JUGNsk) тоже организуем свою Java-конференцию SnowOne в Новосибирске. В нашем программном комитете много системщиков, поэтому докладам о внутренностях виртуальных машин на конференции уделяется особое внимание.

Twitter

Ещё один источник свежей информации — Twitter. Часто там появляются последние новости о Java вообще и JVM в частности. Из англоязычных авторов советую подписаться на аккаунты Брайана Гетца и Марка Рейнхольда — архитекторов языка Java и Java-платформы.

В России тоже есть суперзвёзды и разработчики JVM, например Алексей Шипилёв. Он один из главных контрибьюторов Shenandoah GC, работал в Oracle, а сейчас — в Red Hat. При этом он пишет классные посты (чего стоит одна только серия JVM Anatomy Quarks!) и выступает с отличными докладами о том, как устроена JVM изнутри.

Ещё всегда интересно читать Сергея Куксенко — перформанс-инженера в Oracle. Он разгоняет многие фичи, которые появляются в Java, и интересно об этом рассказывает. А новости о HotSpot и Panama можно найти в аккаунте Владимира Иванова.

В целом в Twitter есть ещё огромное количество как русскоязычных, так и англоязычных крутых инженеров, связанных с разработкой JVM. Так что принцип здесь такой же, как с мейлинг-листами: находите человека, участвующего в определённом проекте в OpenJDK (или за его пределами), и смело подписываетесь!

Резюме:

  • Чтобы решать рутинные задачи, джависту не нужно разбираться в деталях JVM. Но перейти на следующий уровень не получится, если хотя бы на концептуальном уровне не понимать, как работает интерпретатор, что такое Just In Time Compilation, как устроена память и так далее.
  • JDK — это комплект инструментов Java-разработчика. В него входит: javac, сама виртуальная машина, стандартная библиотека классов и документация. OpenJDK — это эталонная версия JDK с полностью открытым исходным кодом. Основные контрибьюторы в этот проект — это Oracle, но кроме неё есть и много других компаний и разработчиков, которые вносят свой вклад.
  • Обычно виртуальные машины пишут на языках C/C++. Но уже есть проекты, в которых JVM, компилятор и рантайм почти полностью написаны на Java или его диалекте.
  • Контрибьютить в OpenJDK может любой желающий, но серьёзные изменения в JVM обычно вносят опытные разработчики. Тем не менее возможно найти себе небольшие задачи, с которых и начать этот путь.

Читайте также:

  • Чем Rust отличается от плюсов: откровение ветерана C++
  • Есть ли место творчеству в программировании: подборка мнений опытных разработчиков
  • Дженерики в Java для тех, кто постарше: стирание типов, наследование и принцип PECS

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *