Введение в байт-код Java
Каждому Java-разработчику известно, какую роль в экосистеме языка играет JVM. Однако большинство не разбирается в том, как работает JVM под капотом. Хотя для разработки на Java это не обязательно, код станет лучше, если вы глубже поймете JVM, потому что так вы будете знать, как каждая строка кода влияет на процессы внутри JVM.
Однако для начала нужно понять, что такое байт-код. Итак, поговорим о вводе и выводе байт-кода Java и о том, как он влияет на JVM во время запуска программы.
Что такое байт-код Java?
Если в какой-то момент профессиональной жизни вы слышали, как проповедуют независимость Java-программ от платформ, скажите спасибо байт-коду.
Байт-код — это набор команд, который JVM применяет для запуска программы. Поскольку байт-код, сгенерированный для программы, не зависит от платформы, где она запущена, вы можете без проблем запускать свою программу на любой машине, на которой есть JVM для интерпретации байт-кода.
Как генерируется байт-код?
Байт-код — это просто результат компиляции класса Java. Файл .class на самом деле представляет собой набор инструкций байт-кода, в которые преобразуется код. Он нуждается в интерпретаторе, таком как JVM, чтобы понимать и выполнять инструкции.
Как посмотреть байт-код Java?
Если вы пытались открыть файл .class , то по опыту должны знать, что это невозможно без декомпилятора. Однако с декомпилятором вы на самом деле видите не байт-код, а Java-код, в который декомпилятор ретранслирует байт-код.
Если вам хочется увидеть сам байт-код, простейший способ — воспользоваться командной строкой.
Следующая команда позволит увидеть фактический байт-код файла .class .
Какие здесь флаги?
- -c нужен для дизассемблирования класса Java.
- -p нужен для раскрытия закрытых членов класса.
- -v нужен для просмотра подробной информации, такой как размер стека и пул констант.
Как работает JVM
Прежде чем углубляться в байт-код, стоит понять, как JVM его обрабатывает.
Методы — одна из важнейших составляющих кода для JVM. Среда выполнения Java-программы — это, по сути, набор методов, вызываемых JVM. JVM создает фрейм для каждого такого метода и помещает созданный фрейм наверх стека текущего потока для выполнения.
Фрейм состоит из локальной среды, которая необходима для поддержания его выполнения. Как правило он содержит массив локальных переменных и стек операндов. Посмотрим, что эти элементы из себя представляют.
Массив локальных переменных
Массив локальных переменных, как следует из названия, нужен для хранения локальных переменных в методе. Также он хранит аргументы, которые принимает метод.
В массиве локальных переменных с индексацией от нуля первые индексы используются для хранения аргументов метода. После того, как они будут сохранены, в массив сохранятся другие локальные переменные. Если метод — не статический, а создаёт экземпляры, нулевой индекс будет зарезервирован для хранения ссылки this , которая указывает на экземпляр объекта для вызова метода.
Определим два метода: один статический и один метод экземпляра, но схожие во всем остальном.
Локальные массивы переменных для этих методов будут выглядеть следующим образом:
Стек операндов
Стек операндов — это рабочее пространство внутри фрейма метода. Поскольку это стек, вы можете помещать и забирать значения только из верхней его части. Большинство инструкций байт-кода, принадлежащих определенному методу, либо участвуют в помещении значений в стек, либо забирают значения из стека для обработки.
Инструкция байт-кода load и ее расширения нужны для перемещения значения, хранящегося в массиве переменных, в стек. Инструкция store применяется для извлечения значений из стека и сохранения в массиве переменных. Существуют и другие инструкции, которые извлекают значения из стека для обработки.
Пример такого сценария — команда add , которая извлекает два самых верхних значения из стека и складывает их вместе, а также инструкции вызова метода, которые извлекают самые верхние значения (число зависит от количества параметров, принятых методом) из стека, чтобы передать их в качестве аргументов методу. Если после выполнения команд будут получены результирующие значения, они будут помещены обратно в стек.
Посмотрим в байт-код
Ради возможности вглядеться в байт-код, я написал простой Java-класс:
Скомпилируем класс с помощью команды javac и посмотрим байт-код с помощью javap . Результат выглядит так:
Посмотрев на инструкции байт-кода, вы обнаружите несколько знакомых команд, включая load и const . Остальное, однако, может даже сбить с толку.
Деконструкция байт-кода
Все не так страшно, как кажется. Попробуем деконструировать байт-код SimpleClasсs шаг за шагом. Начнем с самого простого метода — isEven .
Вот его байт-код:
- Во-первых, инструкция iload_1 помещает значение массива локальных переменных с индексом 1 в стек операндов. Поскольку метод isEven является методом экземпляра, ссылка на него хранится в нулевом индексе. Тогда легко понять, что значение, хранящееся в индексе 1, на самом деле будет принятым значением параметра int .
- iconst_2 помещает значение 2 в верхнюю часть стека операндов.
- Инструкция irem применяется для нахождения остатка от деления между двумя числами. Это инструкция, которая представляет логику оператора % . Она извлекает два самых верхних значения в стеке и помещает результат обратно в стек.
- Команда ifne сообщает JVM перейти к инструкции с заданным смещением (в данном случае — 10), если значение, обрабатываемое командой, не равно 0. Для реализации этой логики команда берет верхний элемент стека. Если переданное число было четным, то верхний элемент будет равен 0, и в этом случае JVM получает команду перейти к инструкции с индексом 6. Однако, если значение стека не равно нулю, что происходит, когда число нечетное, JVM переходит к инструкции с индексом 10.
- iconst_1 помещает значение int 1 в стек. Это происходит только в том случае, если результат irem равен 1. Здесь 1 представляет логическое значение true .
- goto говорит JVM перейти к инструкции, приведенной в смещении, что в данном случае равно 11. Инструкция goto применяется для перехода с одного места в таблице инструкций на другое.
- iconst_0 помещает в стек значение 0. Эта инструкция идет в дело, когда условие if оказывается ложным. Переданное значение 0 действует как логическое значение false . Инструкции 3, 6, 7 обрабатывают случай, когда условие if истинно.
- ireturn возвращает значение int в верхней части стека.
Здесь важно отметить еще одно: индексы, заданные инструкциям байт-кода — как видим, они не увеличиваются на единицу для каждой новой инструкции.
Число перед инструкцией указывает на индекс ее начального байта. А любой байт-код состоит из однобайтовых опкодов, за которыми следует ноль или более операндов.
Опкоды — это такие команды, как iload , iconst и т.д. В зависимости от размера операндов размер байт-кода может варьироваться от одного байта до нескольких. Отсюда и пробелы в индексах таблицы инструкций. Единственная здесь двухбайтовая инструкция — ifne .
В байт-коде SimpleClass.class есть другие инструкции, такие как invokespecial , invokeinterface и invokestatic , которые в свою очередь являются инструкциями вызова метода.
Вывод
Надеюсь, вам удалось узнать кое-что новое о том, как работает байт-код Java. С этим более четким знанием вы сможете лучше писать код. Можете даже поэкспериментировать с самим байт-кодом во время выполнения программы, воспользовавшись такими библиотеками, как ASM.
Java Virtual Machine
Java Virtual Machine (сокращенно Java VM, JVM) — виртуальная машина Java — основная часть исполняющей системы Java, так называемой Java Runtime Environment (JRE). Виртуальная машина Java интерпретирует Байт-код Java, предварительно созданный из исходного текста Java-программы компилятором Java (javac). JVM может также использоваться для выполнения программ, написанных на других языках программирования. Например, исходный код на языке Ada может быть откомпилирован в байт-код Java, который затем может выполниться с помощью JVM.
JVM является ключевым компонентом платформы Java. Так как виртуальные машины Java доступны для многих аппаратных и программных платформ, Java может рассматриваться и как связующее программное обеспечение, и как самостоятельная платформа, отсюда принцип «написано однажды, запускается везде» (write once, run anywhere). Использование одного байт-кода для многих платформ позволяет описать Java как «скомпилировано однажды, запускается везде» (compile once, run anywhere).
Содержание
Спецификация JVM
В 1996-м году компания Sun выпустила первую версию документа «Голубая книга JVM», в котором описана спецификация виртуальной машины Java, ставшего де-факто отраслевым стандартом платформы Java. Благодаря этому документу появились альтернативные реализации JVM, являющиеся «разработками с чистого листа» (англ. clean room design ). В качестве примера можно привести Kaffe.
Начиная с версии J2SE 5.0 изменения в спецификации JVM вырабатываются в соответствии с формализованными пожеланиями заинтересованных сторон. Процесс внесения изменений в спецификации JVM называется Java Community Process.
JVM, доступная в исходных текстах на Си от фирмы Sun называется KVM (Kilo Virtual Machine) и доступна на их сайте.
Конкуренция между Sun и Microsoft
В начале развития платформы Java существовали две конкурирующие реализации Java VM — одна от фирмы Sun Microsystems, создателя языка Java, для различных платформ (Windows, Mac OS, Unix), и другая — от фирмы Microsoft, ориентированная только на платформу Windows и, по утверждениям Microsoft, «специально оптимизированная для быстрого выполнения Java-кода на платформе Microsoft Windows».
Однако, Microsoft JVM была не полностью совместима со спецификацией, описанной Sun в голубой книге JVM, а также имела существенные проблемы с производительностью при работе под большими нагрузками (при большом числе одновременно выполняемых потоков) и с безопасностью. Компания Sun посчитала такую ситуацию недопустимой и решила, что Microsoft занимается намеренной дискредитацией и профанацией платформы Java путем распространения своей версии виртуальной машины Java, обладающей вышеперечисленными недостатками. На этом основании Sun неоднократно подавала в суд на Microsoft и добилась сначала того, что Microsoft потеряла право называть свою реализацию JVM Java VM. С этого момента Microsoft стала называть свой продукт просто Microsoft VM. Затем Microsoft потеряла право встраивать свою VM в браузеры и операционные системы. После этого Microsoft вынуждена была изъять свою VM из поставки Windows и из пакета IE, но могла, согласно решению суда, предлагать её отдельно для скачивания на сайте. Затем Sun добилась прекращения разработки новых версий Microsoft VM и прекращения выпуска обновлений к ней, а затем и снятия Microsoft VM из списка доступных для скачивания программ на сайте Microsoft.
Некоторые обозреватели компьютерных изданий полагают, что жесткая позиция, занятая Sun по вопросу Java, могла послужить дополнительным стимулом для разработки компанией Microsoft собственного Windows-платформенного решения — «.NET Framework».
Разногласия между Sun и IBM
В 2001 году, с целью разработки стандарта кросс-платформенных Desktop-приложений, IBM стартовала открытый проект Eclipse.
Фреймворк Eclipse был основан на предыдущей закрытой разработке IBM VisualAge. IBM удалось сбалансировать интересы свободного сообщества и интересы бизнеса (свои интересы) в лицензии Eclipse Public License, признанной организацией FSF.
Проект успешно развивается, используется в индустрии, в значительной степени отделился от IBM в самостоятельный (см. Eclipse Foundation).
Sun остаётся в оппозиции к Eclipse Foundation, также как и Microsoft. Формально основной причиной противоречий остается библиотека SWT, которая противоречит Sun-концепции виртуальной машины и переносимости Java-приложений.
Среда исполнения
Программы, предназначенные для запуска на JVM должны быть скомпилированы в стандартизированном переносимом двоичном формате, который обычно представляется в виде файлов .class. Программа может состоять из множества классов, размещенных в различных файлах. Для облегчения размещения больших программ, часть файлов вида .class могут быть упакованы вместе в так называемый .jar файл (сокращение от Java Archive).
Виртуальная машина JVM исполняет файлы .class или .jar, эмулируя инструкции, написанные для JVM, путем интерпретирования или использования just-in-time компилятора (JIT), такого, как HotSpot от Sun microsystems. В наши дни JIT компиляция используется в большинстве JVM в целях достижения большей скорости. Существуют также ahead-of-time компиляторы, позволяющие разработчикам приложений прекомпилировать файлы классов в родной для конкретной платформы код.
Как и большинство виртуальных машин, Java Virtual Machine имеет stack-ориентированную архитектуру, свойственную микроконтроллерам и микропроцессорам.
JVM, которая является экземпляром JRE (Java Runtime Environment), вступает в действие при исполнении программ Java. После завершения исполнения, этот экземпляр удаляется сборщиком мусора. JIT является частью виртуальной машины Java, которая используется для ускорения времени выполнения приложений. JIT одновременно компилирует части байт-кода, которые имеют аналогичную функциональность, и, следовательно, уменьшает количество времени, необходимого для компиляции.
5 лучших программ, которые интерпретируют байт-код Java, который вы можете получить сегодня
Вы разработчик программного обеспечения или программист? Вам нужен надежный интерпретатор байт-кода Java? Эта статья специально для вас.
В этой части мы рассмотрим некоторые из наиболее надежных программ, которые интерпретируют байт-код Java.
Очевидно, что Java является одним из наиболее важных аспектов программирования, так как большое количество программ / приложений написано на языке Java. Набор инструкций, составляющих Java, является так называемым байт-кодом, который представляет собой набор машинных кодов (обычно в форме файлов .class), предоставляемых виртуальной машине JVM-Java. Виртуальные машины Java обычно называются интерпретаторами байт-кода Java.
В качестве функции программирования трудно найти надежное программное обеспечение, которое интерпретирует байт-код Java. Тем не менее, мы провели некоторое исследование и составили для вас полный список лучших интерпретаторов байт-кода Java на рынке, особенно совместимых с операционной системой Windows.
Читайте дальше, поскольку мы представляем вам исчерпывающую схему 5 лучших программ, которые интерпретируют байт-код Java.
Лучшее программное обеспечение, которое интерпретирует байт-код Java
HotSpot
HotSpot от Oracle является ведущим выбором большинства разработчиков по всему миру. И это возглавляет наш список программного обеспечения, которое интерпретирует байт-код Java.
Программа, которая первоначально известна как Java HotSpot Performance Engine , написана на C ++ и языке ассемблера. Он также поддерживает кроссплатформенную поддержку операционных систем Windows, Mac, Linux и Solaris.
HotSpot обладает мощным набором функций, в частности, функциями «адаптивной оптимизации» и «своевременной компиляции».
Кроме того, HotSpot поддерживает только архитектуры набора команд (ISA), такие как x86-84, IA-32, VFP ARMv7, SPARC (эксклюзивно для компьютеров Solari) и ARMv8. Эта система поддержки ISA, как правило, лучшая на рынке.
Другие известные особенности HotSpot включают интерпретатор байт-кода Java, двойную виртуальную машину (компилятор клиента и сервера), многоуровневый компилятор, загрузчик классов Java, сборщики мусора, библиотеки времени выполнения, флаги JVM, кросс-платформенную совместимость и многие другие.
Интерпретатор байт-кода Java HotSpot и виртуальная машина лицензируются в соответствии с GNU General Public License (GPL). Как таковой, он доступен бесплатно.
OpenJ9
OpenJ9 от Eclipse Foundation IBM широко считается ближайшим конкурентом HotSpot. Как и HotSpot, это также универсальный интерпретатор байт-кода Java и виртуальная машина Java.
Программное обеспечение совместимо со всеми основными компьютерными операционными системами, в частности с Windows, AIX, macOS, Linux и многими другими. Существует специальная поддержка — встроенный двоичный файл OpenJDK — для трех известных компьютерных ОС, то есть Windows, Linux и macOS.
Этот программный интерпретатор предлагает оптимизированную настройку, которая адекватно оснащена, чтобы не только интерпретировать байт-код Java, но также повышает общую производительность вашей Java-программы.
Кроме того, программное обеспечение предлагает настраиваемую платформу, которая позволяет вам создавать OpenJDK самостоятельно, при условии, что у вас есть необходимый опыт.
Другие заслуживающие внимания особенности виртуальной машины OpenJ9 / интерпретатора байт-кода Java включают в себя объем памяти, быстрый запуск и время выполнения, расширенную конфигурацию, Eclipse OMR (встроенная программа), компиляторы Just-in-time (JIT) и Ahead of time (AOT), дампы Java , системные дампы, онлайн ресурсы и многое другое.
OpenJ9 — это приложение с открытым исходным кодом, которое финансируется за счет пожертвований со всего мира. Точно говоря, OpenJ9 доступен для бесплатной загрузки.
JRockit
JRockit является одной из наиболее широко используемых виртуальных машин Java и выделяется как одна из лучших программ, интерпретирующих байт-код Java. Программное обеспечение, которое изначально разрабатывалось виртуальной машиной Appeal, теперь принадлежит и управляется Oracle .
Он поддерживается в Windows и других известных операционных системах. Кроме того, программное обеспечение поддерживается только в системах со следующими ISA: SPARC (для Solaris), x86-64 и x86.
JRockit написан на кодах C и Java и служит универсальным решением JVM — виртуальная машина Java -, которое точно разработано для высокой производительности, мониторинга, интерпретации и диагностики программ Java.
Кроме того, JRockit оптимизирован для обеспечения быстрого выполнения приложений Java. Другие примечательные особенности Oracle JRockit включают интерпретацию байт-кода Java, сборку мусора, мягкую настройку, анализ программ, быстрое время отклика (в микросекундах) и многое другое.
JRockit стал свободным программным обеспечением в 2011 году, вскоре после его приобретения Oracle. И теперь вы можете скачать его бесплатно.
Squawk Виртуальная машина
Виртуальная машина Squawk является выдающейся JVM, которая предназначена для создания простых приложений Java. Это микро версия JVM, специально разработанная для простоты, с небольшим внешним вмешательством.
Следовательно, он лучше всего приспособлен для небольших устройств и встроенных систем. Интерпретатор байт-кода Java является кросс-совместимым со всеми известными операционными системами, в основном Windows, Linux и macOS.
Squawk, как и большинство интерпретаторов / виртуальных машин Java-байт-кода, написан на низкоуровневых языках программирования, таких как C ++ и Assembly. Тем не менее, основные аспекты программного обеспечения написаны на Java, что делает его «метакруглым интерпретатором».
Другие ключевые функции Squawk включают в себя низкий объем памяти, интерпретацию байт-кода, упрощенный интерфейс, простую отладку, межплатформенную поддержку, простую интеграцию и многое другое.
Squawk лицензируется в соответствии с GNU General Public License (GPL), и он доступен бесплатно.
Апач Гармония
Apache Harmony от Apache Software — еще одно популярное программное обеспечение, которое интерпретирует байт-код Java. Хотя разработка программного обеспечения остановилась в 2011 году, она все еще остается одним из лучших интерпретаторов байт-кода Java и виртуальных машин.
Apache Harmony поддерживается в Windows, Linux и macOS. Существует также мобильная поддержка Android, которая теперь работает в основном на библиотеках OpenJDK, а не в качестве полноценной виртуальной машины.
Гармония написана на C ++ и Java. А до официального выхода на пенсию в 2011 году программное обеспечение не дотягивало до 2%.
Следовательно, неполная библиотека привела к прекращению работы некоторых программ на Java, таких как Vuze, Geronimo (из Apache) и ArgoUML.
Тем не менее, несмотря на внезапное завершение и почти полноту, ряд приложений по-прежнему поддерживается в Harmony. Эти приложения включают в себя JUnit, Tomcat, Apache Velocity, Apache Ant и многие другие.
Другие функции Apache Harmony включают интерпретатор байт-кода Java, библиотеку классов, JIT-компилятор, ядро виртуальной машины, уровень портирования, диспетчер потоков, сборщик мусора и многие другие.
Apache Harmony был удален (его разработка остановлена) в ноябре 2011 года. Однако он все еще доступен для скачивания, хотя и без технической поддержки или чего-то подобного.
В заключение, интерпретаторы байт-кода Java являются важными компонентами при разработке программ на Java. И чтобы помочь вам в выборе, мы рассмотрели пять лучших интерпретаторов байт-кода Java на рынке.
Эти интерпретаторы байт-кода Java были выбраны и ранжированы на основе их относительной популярности и долговечности по сравнению с другими.
Инициализация и работа интерпретатора байткода в JVM HotSpot под x86
Почти каждый Java разработчик знает, что программы, написанные на языке Java изначально компилируются в JVM-байткод и хранятся в виде class-файлов стандартизованного формата. После попадания таких class-файлов внутрь виртуальной машины и пока до них еще не успел добраться компилятор, JVM интерпретирует байткод, содержащийся в этих class-файлах. Данная статься содержит обзор принципов работы интерпретатора применительно к OpenJDK JVM HotSpot.
- Окружение
- Запуск java приложения
- Инициализация интепретатора и перадача управления java-коду
- Пример
Окружение
Для экспериментов используется сборка крайней доступной ревизии OpenJDK JDK12 с autoconf конфигурацией
на Ubuntu 18.04/gcc 7.4.0.
—with-native-debug-symbols=internal означает, что, при сборке JDK, дебажные символы будут содержаться в самих бинарях.
—enable-debug — то, что в бинарнике будет содержаться дополнительный дебажный код.
Сборка JDK 12 в таком окружении — это не сложный процесс. Все, что мне потребовалось проделать это поставить JDK11 (для сборки JDK n требуется JDK n-1) и доставить руками необходимые библиотеки о которых сигналил autoconf. Далее выполнив команду
и немного подождав (на моем ноуте порядка 10 минут), получаем fastdebug сборку JDK 12.
В принципе вполне достаточно было бы просто установить jdk из публичных репозиториев и дополнительно доставить пакет openjdk-xx-dbg с дебажными символами, где xx- версия jdk, но fastdebug сборка предоставляет функции для отладки из gdb, которые могут облегчить жизнь в некоторых случаях. На данный момент я активно использую ps() — функция для просмотра Java-стектрейсов из gdb и pfl() — функция для анализа стек фреймов (очень удобно при отладке интерпретатора в gdb).
Для примера рассмотрим следующий gdb-скрипт
Результат запуска такого скрипта имеет вид:
Как можно видеть, в случае ps() мы просто получаем стек вызовов, в случае pfl() — полную организацию стека.
Запуск java приложения
Прежде чем перейти к рассмотрению непосредственно интерпретатора, сделаем краткий обзор действий, выполняющихся до передачи управления java-коду. Для примера возьмем программу на языке Java, которая «не делает вообще ничего»:
и попробуем разобраться в том, что происходит при запуске такого приложения:
javac Main.java && java Main
Первое что нужно сделать, чтобы ответить на этот вопрос, это найти и посмотреть на бинарник java — тот самый, который мы используем для запуска все наших JVM-приложений. В моем случае он располагается по пути
Но смотреть в итоге тут особо не на что. Это бинарник который вместе с дебажными символами занимает всего 20КБ и скомпилирован только из одного исходного файла launcher/main.c.
Все, что он делает это получает аргументы командной строки (char *argv[]), читает аргументы из переменной среды JDK_JAVA_OPTIONS, делает базовый препроцессинг и валидацию (например, нельзя добавить терминальную опцию или имя Main-класса в эту переменну среды) и вызывает функцию JLI_Launch с полученным списком аргументов.
Опреление функции JLI_Launch не содержится в бинарнике java и, если посмотреть на его прямые зависимости:
то можно заметить libjli.so которая к нему прилинкована. Данная библиотека содержит launcher interface — набор функций, которые используются java для инициализации и запуска виртуальной машины, среди которых присутствует и JLI_Launch.
После передачи управления JLI_Launch происходит ряд действий необходимых для запуска JVM такие как:
I. Загрузка символов JVM HotSpot в память и получение указателя на функцию для создания VM.
Весь код JVM HotSpot располагается в библиотеке libjvm.so. После определения абсолютного пути к libjvm.so происходит загрузка библиотеки в память и выдирание из нее указателя на функцию JNI_CreateJavaVM. Этот указатель на функцию сохраняется и в дальнейшем используется для создания и инициализации виртуальной машины.
Очевидно, что libjvm.so не прилинкована к libjli.so
II. Парсинг аргументов, переданных после препроцессинга.
Функция с говорящим названием ParseArguments разбирает аргументы, переданные из командной строки. Этот парсер аргументов определяет режим запуска приложения
Также он преобразует часть аргументов в формат -DpropertyName=propertyValue , например -cp=/path приводится к виду -Djava.class.path=/path . Далее такие SystemProperty сохраняются в глобальном массиве в JVM HotSpot и пробрасываются в java.lang.System::props в первой фазе инициализации (В JDK12 механизм инициализации java.lang.System.props был модифицирован, подробнее в этом коммите).
Парсинг аргументов также отбрасывает часть опций, которые не обрабатываются JVM (например —list-modules , обработка данной опции происходит непосредственно в launcher’e в этом месте).
III. Форк primordial потока и создание в нем VM
Но если что-то пошло не так, то делается попытка запустить JVM в main-треде «just give it a try».
Поизучав вопрос, я нашел одну из возможных причин, по которой JVM запускается не в main-треде. Дело в том, что (по крайней мере в Linux) pthread’ы и main-тред работают со стеком по разному. Размер main-thread’a ограничен значением ulimit -s , т.е. при выставлении сколь угодно большого значения мы получим сколь угодно большой стек. Main-тред использует нечто похожее на MAP_GROWSDOWN, но не MAP_GROWSDOWN . Использование MAP_GROWSDOWN в чистом виде не безопасно и, если мне не изменяет память, задепрекейчено. На моей машине MAP_GROWSDOWN не добавляет никакого эффекта. Отличие маппинга main-треда от MAP_GROWSDOWN в том, что никакой другой mmap , за исключением MAP_FIXED , не сможет сделать коллизию с областью возможного расширения стека. Все что нужно от софта — это выставить соответствующее значение rsp и дальше ОС сама разберется: И page-fault обработает и guard выставит. Такое различие пораждает некоторое количество граблей: При определении размера стека текущего потока, при создании guard-pages
Итак, будем считать, что на данный момент у нас успешно распарсились опции и создался поток для VM. После этого, только что форкнутый поток начинает создание виртуальной машины и попадает в функцию Threads::create_vm
В этой функции делается довольно большое количество черной магии инициализаций, нам интересны будут лишь некоторые из них.
Инициализация интепретатора и передача управления java-коду
Для каждой инструкции в JVM HotSpot существует определенный шаблон машинного кода под конкретную архитектуру. Когда интерпретатор приступает к выполнению какой-либо инструкции, первым делом ищется адрес ее шаблона в специальной таблице DispatchTable. Далее происходит jump по адресу данного шаблона и после того как выполнение инструкции завершено, jvm достает адрес следующей по порядку инструкции) и начинает выполнять ее аналогичным образом, и так далее. Такое поведение наблюдается у интерепретатора только для инструкций, которые не «делают dispatch», например, арифметические инструкции ( xsub , xdiv , etc, где x — i , l , f , d ). Все, что они делают — это выполняют арифметические операции.
В случае инструкций вызова процедур ( invokestatic , invokevirtual , и т.д.) следующей к выполнению инструкцей будет первая по порядку инструкция вызываемой процедуры. Такие инструкции самостоятельно проставляют адрес следующей bytecode-инструкции к выполнению в своем шаблоне.
Чтобы обеспечить работу данной машинерии в Threads::create_vm выполняется ряд инициализаций, от которых зависит интерпретатор:
I. Инициализация таблицы доступных байткодов
Прежде чем приступить к инициализации интерпретатора, необходимо проинициализировать таблицу используемых байткодов. Она выполняется в функции Bytecodes::initialize и представлена в виде очень удобочитаемой таблички. Ее фрагмент выглядит следующим образом:
В соответствии с данной таблицей, для каждого байткода выставляются его длина (размер всегда 1 байт, но может быть еще индекс в ConstantPool , а также широкие байткоды), имя, байткод и флаги:
Эти параметры в дальнейшем нужны для генерации кода шаблонов интерпретатора
II. Инициализация код кэша
Для того, чтобы сгенерить код шаблонов интерпретатора, необходимо сперва выделить под это дело память. Резервация памяти под код кэш реализована в функции с одноименным названием CodeCache::initialize(). Как можно видеть из следущего участка кода данной функции
код кэш контролируется опциями -XX:ReservedCodeCacheSize , -XX:SegmentedCodeCache , -XX:CodeCacheExpansionSize , -XX:NonNMethodCodeHeapSize , -XX:ProfiledCodeHeapSize , -XX:NonProfiledCodeHeapSize . Краткое описание данных опций можно посмотреть по ссылкам на которые они ведут. Помимо коммандной строки, значения некоторых из этих опций подстраивается эргономикой, например, если используется значение SegmentedCodeCache по умолчанию (выключен), то при размере кода >= 240Mb , SegmentedCodeCache будет включен в CompilerConfig::set_tiered_flags.
После выполнения проверок резервируется область размером в ReservedCodeCacheSize байт. В случае, если SegmentedCodeCache оказалась выставленной, то данная область разбивается на части: JIT-скомпилированные методы, стаб рутины, и т.д.
III. Инициализация шаблонов интерпретатора
После того, как таблица байткодов и код кэш проинициализированы, можно приступать к кодогенерации шаблонов интерпретатора. Для этого интерепретатор резервирует буффер из ранее проинициализированного код кэша. На каждый этап кодогенерации из буффера будут отрезаться кодлеты — небольшие участки кода. После завершению текущей генерации, неиспользуемая под код часть кодлета освобождается и становится доступной для последующих кодогенераций.
Рассмотрим каждый из этих этапов по отдельности:
signature handler используется для подготовки аргументов для вызовов нативных методов. В данном случае генерится обощенный хэндлер, если, например у нативного метода больше 13 аргументов (В дебаггере не проверял, но судя по коду должны быть так)
VM валидирует классфайлы при инициализации, но это на случай, если аргументы на стеке не того формата который нужен или байткод о котором VM не знает. Эти стабы используются при генерации кода шаблонов для каждого из байткодов.
После вызова процедур необходимо восстановить данные стек фрейма, который был до вызова процедуры из которой делается return.
Используется при вызовах рантайма из интерепретатора.
Представлен в виде макроса в зависимости от типа метода. В общем случае выполняется подготовка интерпретируемого стек-фрейма, проверка StackOverflow, stack-banging. Для нативных методов определяется signature handler.
Для выполнения инструкции спецификация VM требует чтобы операнды находились в Operand Stack, но это не запрещает HotSpot кэшировать их в регистре. Для определения текущего состояния вершины стека используется перечисление
Каждая инструкция определяет входные и выходные состояния TosState вершины стека, и генерация шаблонов происходит в зависимости от этого состояния. Данные шаблоны инициализируются в удобочитаемой таблице шаблонов. Фрагмент этой таблицы выглядит следующим образом:
Нам будут особенно интересны столбцы in , out и generator .
in — состояние вершины стека на момент начала исполнения инструкции
out — состояния вершины стека на момент завершения исполнения инструкции
generator — генератор шаблона машинного кода инструкции
Общий вид шаблона для всех байткодов можно описать в виде:
Если для инструкции не выставлен dispatch bit, то выполняется пролог инструкции (no-op на x86)
Используя generator , генерится машинный код
Если для инструкции не выставлен dispatch bit, то выполняется переход к следующей по порядку инструкции в зависимости от out состояния вершины стека, которое будет являтся in для следующей инструкции
Адрес точки входа для полученного шаблона сохраняется в глобальной таблице и его можно использовать при отладке.
В HotSpot за это отвечает следущий, относительно стремный кусок кода:
Как только данная кодогенерация завершена, интепретатор можно считать полностью проинициализированным. После интерпретатора выполняется еще много инициализаций различных подсистем JVM. Для некоторых из них требуется вызывать Java-код из кода виртуальной машины. Это реализовано с помощью стандартного механизма JavaCalls. После того как инициализация JVM полностью завершена, этот механизм используется для вызова метода main.
Пример
Для того, чтобы представлять как это все работает на практике, рассмотрим следующий относительно простой пример:
и попытаемся понять что происходит при вызове метода Sum.sum(II) .
Скомпилируем эти 2 класса javac -c *.java и убедимся в том, что компилятор не сделал никаких оптимизаций.
Байткод Sum.sum :
Байткод ровно такой, какой нам нужен и первое с чего придется начать — это с анализа вызова статического метода.
Генератор шаблона invokestatic ‘а для x86 находится в архитектурно-зависимой секции кода HotSpot и представлен в виде
byte_no == f1_byte — это секция ConstantPoolCache , относящаяся к статическим методам, rbx — регистр, в котором будет храниться указатель Method * . В остальном все в принципе понятно: Подготовка вызова, профайлинг, переход на точку входа метода ( method_entry при генерации шаблонов интерпретатора).
Рассмотрим подронее prepare_invoke . Как известно, следом за байткодом инструкции invokestatic идет индекс в ConstantPool на Constant_Methodref_Info . В случае HotSpot это не совсем так. Следущие 2 байта указывают на индекс в т.н. ConstantPoolCache . ConstantPoolCache это структура данных в которой хранится информация, нужная для интерпретатора (например, было ли зарезолвено ConstantPoolCacheEntry по данному индексу, реализуя таким образом ленивость загрузки классов). После того как это ConstantPoolCacheEntry зарезолвено, в него записывается номер байткода (изначально там был 0) и этот номер используется при дальнейшем определении зарезолвено/не зарезолвено. Несмотря на то, что при загрузке класса индексы изначально указывают в ConstantPool , при линковке класса они будут перезаписаны на ConstantPoolCache индексы в нативном байт ордере (на x86 Little Endian).
Итак, первое, что HotSpot пытается сделать в prepare_invoke — это достать индекс на ConstantPoolCache . После того, как индекс получен, делается проверка на зарезолвенность ConstantPoolCacheEntry по данному индексу
Если нет, значит нужно вызывать InterpreterRuntime::resolve_from_cache .
В данной функции выполняется загрузка класса receiver’a вызываемого статического метода, если на данный момент класс еще не был загружен. После загрузки выполняется инициализация (линковка, валидация, перезаписывание байткода, создание ConstantPoolCache и вызов <clinit> , если такой метод присутствует в байткоде). Не ленивая инициализация может выполняться и сразу после define class, если выставлен флаг EagerInitialization (флаг девелоперский, поэтому из коммандной строки не доступен, но кто нам запретит у себя его поменять на продакшн :)). Вообще загрузка классов в HotSpot в общем (и CDS в частности) имеет относительно не тривиальную реализацию.
После того, как класс инициализирован и нужный метод в классе найден, инициализируется соответветствующее ConstantPoolCacheEntry этими байткодом и методом. После этого интерпретатор загружает указатель Method * в rbx , достает адрес возврата, делает профайлинг и переходит на точку входа вызовав метода.
Исследуем теперь точку входа при вызове Sum.sum(2, 3) . Для этого нам потребуется следующий gdb-script sum.gdb :
Запустив данный скрипт gdb -x sum.gdb , останавливаемся на точке входа в метод Sum.sum
Если открыть layout asm , то мы увидим код, сгенеренный методом generate_normal_entry. В данном шаблоне делается создание стек-фрейма, проверка StackOverflow, stack-banging и далее делается dispatch на первую инструкцию iload_0 при незакешированной вершине стека. В этом случае код интерпретатора имеет вид:
После этого вершина стека оказалась закешированной в rax , а значит интерпретатор переходит в следующий шаблон
Ну а теперь и сама инструкция iadd :
Если посмотреть в gdb на eax и edx сразу перед выполнением сложения, то можно заметить