Найти тему

Для чего VM, и каким образом строится память: Контекст в JVM

Контекст в JVM

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

  • Загрузчик классов (вход в java-машину, получение мета-информации о классе, верификация, понимание, что внутри класса написано)
  • Система исполнения байткода, состоящая обычно из интерпретатора, скомбинированного с динамическим профилировщиком, и система трансляции во время исполнения, то бишь JIT
  • Система управления памятью (что в народе называется garbage collector, что странно, ведь управление памятью -- это не только ее освобождение, но еще и выделение)
  • Система дефрагментации памяти и так далее

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

Как мы достигаем полностью предсказуемого контектса в JVM? Почему JVM-языки (тот же Kotlin, который до появления JS и Native был полностью JVM-языком), вход которого -- это некоторый текстовый файл на каком-то языке высокого уровня, а выходом является тот самый байткод, хорошо специфированный формат описания программы в некотором таком объектно-ориентированном мире? Из чего состоит этот объектно-ориентированный мир в JVM?

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

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

  • Объектная модель -- классы, интерфейсы, объекты и методы. Одна из таких популярных парадигм программирования -- это ООП -- попытка рассуждать обо всем как об объекте.
Вообще, большинство парадигм в программировании -- это Everything is a ... . Например, парадигма юникса -- Everything is a file. ФП -- everything is a function. ООП -- everything is an object.

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

В рамках этой объектной модели (а Java придумывали как раз тогда, когда ООП было основным способом думать, ФП было сугубо академическим развлечением, поэтому в оригинальной Java функциональных конструкций особо не присутствовало, и все было объектом). Поэтому в формате байткода описывается достаточно точно некоторый набор объектно-ориентированных взаимоотношений (данного .class-файла с другими .class-файлами, вызова тех или иных методов и т.д.)

Стековая машина отличается от регистровой. Характеристика практически любого набора инструкций -- это то, что кодирует инструкция, в каком наборе понятий выражена семантика данной инструкции. Например, если посмотреть на intel x86 manual, то там можно увидеть, что инструкция add -- это сложение значения source регистра с аргументом и помещение назад в тот же регистр. Здесь параметром описания являются имена регистров. У регистровой машины есть набор регистров (предсказуемое количество, не очень большое, регистров), и над ним регистровая машина производит различные манипуляции.
Если посмотреть на чистые регистровые машины (например, про RISC-процессоры типа ARM, SPARC), то у них все операции можно производить исключительно над значениями регистра. Если что-то есть в памяти, то сначала значение надо загрузить в регистр, затем выполнить операцию над регистром, затем, если надо сохранить, то значение регистра сохраняется в памяти.
Операнды инструкций стековой машины динамически переименовываются. Грубо говоря, операндами всегда являются 2-3-4 верхних элемента стека, и причем какие конкретно элементы стека сейчас наверху, зависит исключительно от глубины стека.

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

Кроме этого, в JVM существуют стандартизированные типы данных (то есть, JVM не является чисто объектно-ориентированной системой, потому что изначально ориентировалась для embedded-приложений, еще в ранние годы Sun, поэтому там пришлось много вкладывать в эффективность). На самом деле, в Java не все -- объект. Есть несколько выделенных типов (мы их знаем как примитивные), которые представляют примитивными значениями, и ими можно манипулировать непосредственно. Тот же IADD не складывает объекты типа Integer, а складывает intы.

В .class-файле записано вот что. Я класс Foo. Мой суперкласс -- это класс Bar (причем ссылка на суперкласс символическая -- сугубо по имени, просто в constant pool написана строчка Foo и строчка Bar и ссылка в соответствующем элементе метаинформации на соответствующее место в constant pool). Я реализую интерфейсы I1, I2, I3, у меня есть методы m1, m2, m3, и про все поля и методы указаны их полные сигнатуры (и можно по этой метаинформации понять, как слинковаться с данным объектным файлом).

Линковка бывает ранней и поздней. В JVM система из достаточно мелких компонентов, и линковка очень поздняя -- установление отношений между различными классами происходит очень поздно с помощью виртуальной машины. Если мы запустили какую-то программу и в ней сослались на какой-то класс, то происходит процесс динамического разрешения этого самого класса -- по строковому имени находится этот класс и все его транзитивные зависимости (динамическая загрузка классов). После того, как все зависимости найдены и загружены в память VM, только тогда можно начинать исполнение, что частично обеспечивает долгий старт java: при каждом запуске VM происходит подъем достаточно сложного, блогатого контекста.

Поверх этих всех квантов у нас есть абстрактная исполняющая машина, которая умеет, взяв в кач-ве входа .class-файл и нек-рое описание того, что нужно запустить (entry pointer какой-нибудь), загружать требуемый файл и все его зависимости, после чего передавать исполнение на точку входа. И дальше программа исполняется на этой абстрактной стековой машине тем или иным образом до какого-то завершения. При этом в абстрактной машине описан полный lifecycle объектов, полный lifecycle классов, потоки нормального исполнения (controlflow), поток исключительного исполнения (обработка исключений тоже является частью стандарта JVM).

Что является одним из самых величайших достижений Java -- это полностью специфицированная система. Если почитать стандарт языка Си (особенно времен, когда создавалась Java), там до сих пор большая часть естественных операций описана с пометкой UB (например, знаковое переполнение в Си -- до сих пор UB, то есть, взрыв компьютера при умножении больших чисел не запрещен стандартом Си). В JVM все поведение специфицировано (причем все аспекты, в том числе исключительный -- кончилась память, переполнился стек, поделили на 0, разыменовали нулевой указатель, в общем, все классические непонятные ситуации мира Си четко специфицированы в Java и четко описано, что будет при том или ином раскладе).

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

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

Также важной частью экосистемы Java являются компиляторы из высокоуровневых языков (в частности, javac компилирует с Java, еще есть Jython, Groovy, ...)

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

Также есть инструменты мониторинга и профирования (JVM TI -- tooling interface, позволяющий понимать, что на текущий момент JVM исполняет, и на основании этого интерфейса с помощью тех же профиляторов выяснять, какие в программе bottleneck-и, и т.д.)

Все вместе это создает некоторую экосистему (экосистема -- это нечто большее, чем виртуальная машина, так как она описывает не только взаимоотношения компьютеров и компььютерных программ, но и взаимоотношение людей, то есть, это тот мир, в котором происходит разработка софта). Часто VM и гипервизоры связаны с экосистемами, они являются инструментами, которые позволяют эти экосистемы образовывать. Если у нас есть виртуальная машина, то она позволяет обеспечить воспроизводимость контекста, а в результате на нем можно строить экосистему. Для индустриального программирования это очень важный аспект. Грубо говоря, если мы просто пишем программы, то они скорее всего окажутся на свалке истории, а если мы создаем большие экосистемы, то как-то так выясняется, что они очень долго живут сами по себе, причем часто многие программы сейчас пережили своих автором, а есть и такие программы, которые сейчас уже никто не поймет. Экосистема -- это для людей возможность такого вот sustainable функционирования, потому что мы сейчас очень сильно зависим от программы.

Для чего VM, и каким образом строится память:
Контекст в JVM