Java HotSpot JIT компилятор — устройство, мониторинг и настройка (часть 2)
В предыдущей статье мы рассмотрели устройство JIT компилятора и способы мониторинга его работы. В этой статье мы рассмотрим счетчики, которые JVM использует для принятия решения о необходимости компиляции кода, потоки компиляции, оптимизации, выполняемые JVM при компиляции, а также что такое деоптимизация кода.
Счетчики вызовов методов и итераций циклов
Главным фактором влияющим на решение JVM о компиляции какого-либо кода является частота его исполнения. Решение принимается на основе двух счетчиков: счетчика количества вызовов метода и счетчика количества итераций циклов в методе.
Когда JVM исполняет какой-либо метод, она проверяет значения этих двух счетчиков, и принимает решение о необходимости его компиляции. У этого типа компиляции нет официального названия, но часто его называют стандартной компиляцией.
Аналогично, после каждой итерации цикла проверяется значение счетчика цикла и принимается решение о необходимости его компиляции.
При выключенной многоуровневой компиляции стандартная компиляция управляется параметром -XX:CompileThreshold. Значением по умолчанию является 10000. Несмотря на то, что параметр всего один, превышение порога определяется суммой значений двух счетчиков. В настоящее время этот флаг ни на что не влияет (многоуровневая компиляция включена по умолчанию), но знать о нем полезно, ведь существует довольно много унаследованных систем.
Ранее уменьшением значения этого параметра добивались ускорения старта приложения при использовании серверного компилятора, поскольку это приводило к более ранней компиляции кода. Также уменьшение его значения могло способствовать компиляции методов, которые иначе никогда бы не скомпилировались.
Последнее утверждение довольно интересно. Ведь если программа исполняется бесконечно, не должен ли весь ее код в конце концов скомпилироваться? На самом деле не должен, поскольку значения счетчиков не только увеличиваются при каждом вызове метода или итерации цикла, но и периодически уменьшаются. Таким образом, они являются отражением текущего «нагрева» метода или цикла.
Получается, что до внедрения многоуровневой компиляции методы, выполнявшиеся довольно часто, но недостаточно часто, чтобы превысить порог, никогда бы не были скомпилированы. В настоящее время такие методы будут скомпилированы компилятором C1, хотя, возможно, их производительность была бы выше, будь они скомпилированы компилятором C2. При желании можно поиграть параметрами -XX:Tier3InvocationThreshold (значение по умолчанию 200) и -XX:Tier4InvocationThreshold (значение по умолчанию 5000), но вряд ли в этом есть большой практический смысл. Такие же параметры (-XX:TierXBackEdgeThreshold) существуют и для задания пороговых значений счетчиков циклов.
Потоки компиляции
Компиляторы C1 и C2 имеют собственные очереди, каждая из которых может обрабатывается несколькими потоками. Существует специальная формула для вычисления количества потоков в зависимости от количества ядер. Некоторые значения приведены в таблице:
Выбор и настройка Garbage Collector для Highload системы в Hotspot JVM
Введение
При работе в сфере RTB (Real Time Bidding) одной из ключевых характеристик является время, затраченное на показ рекламы пользователю, зашедшему на сайт. Оно складывается из нескольких этапов, один из которых – аукцион за рекламное место, проводимый SSP (Supply Side Platform) между несколькими DSP (Demand Side Platform) системами. В этом случае критической величиной является время, за которое DSP успеет ответить своим инвентарем и денежной ставкой за данный показ. Как правило, верхняя граница этого времени составляет примерно 100 миллисекунд. С учетом того, что для оптимальной производительности рекламных кампаний требуется десятки тысяч запросов в секунду, выполнение данного требования может стать весьма нетривиальной задачей.
Наш Ad Server, отвечающий за основную работу GetIntent DSP разработан на языке Java и работает на стандартной Hotspot JVM, имеющей общеизвестные механизмы сборки мусора (GC). Поэтому наиболее оптимальный вариант лежит в анализе того, как именно происходит работа с памятью, и как следствие выбор наиболее подходящего алгоритма сборки мусора и его оптимальная настройка. Об этом и пойдет речь в данной статье.
В совокупности, наш ожидаемый результат – максимальный баланс между количеством серверов (чем меньше, тем лучше) и суммарной продолжительностью и частотой GC пауз, во время которых мы можем терять потенциальные показы.
Как мы тестировали
Для тестирования использовалось 2 рабочих станции. На первой JVM запускалась с:
CMS (Concurrent Mark Sweep)
Тестирование производилось со следующими параметрами:
для логирования использовались:
G1 (Garbage First)
Относительно G1 GC, с момента его появления в качестве доступного для экспериментов сборщика мусора, сформировались некоторые предубеждения, главное из которых то, что MaxGCPauseMillis не выдерживается. Также есть озвученная Oracle рекомендация использовать его на достаточно больших размерах heap ( >= 6 Gb).
Насколько все это актуально мы узнаем после нашего тестирования. Также уделим немного времени такой эксклюзивной для G1 GC функции как String Deduplication.
Тестирование производилось со следующими параметрами:
Дополнительно были проведены тесты с параметром:
для логирования использовались:
Max Heap Size 4.5Gb
Однозначным победителем в данной конфигурации выходит CMS с флагом
Как можно заметить, несмотря на то, что показатель ms/sec паузы у данной конфигурации чуть хуже чем у остальных, она все равно показывает себя как самая стабильная –
12 ms средняя пауза и практически 98% укладывается в норму – отличный для нас результат. При таких показателях на один Full GC в течение 16 часов можно закрыть глаза.
График распределения latency для лучших показателей G1 и CMS:
Анализ результатов CMS
Мы проэкспериментировали с наборами параметров, в которых размер Eden (-XX:NewRatio) был 1/2, 1/4, и 1/6 от общего размера памяти. Средний promotion rate для этих конфигураций распределился соответствующим образом: 1.7, 2.75 и 2.79 mb/sec, что вполне логично – чем меньше размер Eden, тем больше мусора успевает просочиться непосредственно в Old Gen. Как можно заметить, с определенного момента, размер Eden области начинает слабо влиять на этот показатель. В нашем случае мы можем пожертвовать более высоким promotion rate (как следствие более частые OldGen сборки и большая вероятность фрагментации) ради минимально возможной средней задержкой в течении работы приложения.
Анализ результатов G1
Очевидно странный знак. Начиная примерно с age 8 размер остается всегда примерно на одном уровне; это говорит о том, что это долгоживущие объекты, которые скорее всего в любом случае попадут в Tenured область, а до этого при каждой минорной сборке мы просто переливаем из пустого в порожнее, тогда как могли бы сразу поместить все это в OldGen. Хорошее решение – поставить в значение MaxTenuringThreshold=8.
Однако, в случае с heap 4.5Gb мы не заметили большой разницы в результатах, поэтому для краткости опустим их. Посмотрим изменится ли что-то на большом heap.
Max Heap Size 12Gb
Состав представителей G1 немного изменился, т.к. параметр MaxTenuringThreshold=8 (в таблице mtt=8) в данной конфигурации начал приносить заметный результат.
На большом heap G1 расправил крылья и вышел вперед как по общему распределению пауз, так и по очень короткой максимальной паузе. При этом среднее время затрачиваемое на GC составило меньше 7ms каждую секунду, т.е. меньше 0.7%
График распределения latency для лучших показателей G1 и CMS:
Анализ результатов CMS
Считается, что основная проблема CMS это вопрос масштабируемости. Наше тестирование это подтверждают. Практически все показатели хуже, чем при использовании маленького размера heap. Из плюсов можно отметить, что благодаря большему объему памяти, влияния фрагментации здесь заметно ниже – ни одного Full GC за все время эксперимента.
Анализ результатов G1
Результат явно показывает, что G1 действительно гораздо стабильнее на больших объемах памяти; достаточно четко выполняются условия, заданные в настройках. Здесь бесспорный победитель с 40 ms latency. Средняя пауза выросла всего на 3 ms, когда как размер памяти вырос почти в 2.4 раза! Что уж говорить про показатель ms/sec – в два раза лучше.
G1 String Deduplication
Попытки установить меньшую длительность паузы приводили к очень сильному росту потребления CPU. От использования данного параметра было решено отказаться.
Итоги
Мы провели детальный анализ CMS и G1 сборщиков мусора, основной целью которого было понимание того, как сильно мы можем снизить влияние GC на latency – наиболее критичный показатель для нашей системы.
Благодаря этому исследованию мы могли больше не работать вслепую, руководствуясь лишь отдельными рекомендациями и разнородными мнениями; напротив, мы смогли найти идеальный баланс для нашей неоднородной в плане конфигурации системы, получая максимальную стабильность и как следствие прибыль из того, что мы имеем сегодня.
Java HotSpot JIT компилятор — устройство, мониторинг и настройка (часть 1)
AOT и JIT компиляторы
Процессоры могут исполнять только ограниченный набор инструкций — машинный код. Для исполнения программы процессором, она должна быть представлена в виде машинного кода.
Существуют компилируемые языки программирования, такие как C и C++. Программы, написанные на этих языках, распространяются в виде машинного кода. После того, как программа написана, специальный процесс — Ahead-of-Time (AOT) компилятор, обычно называемый просто компилятором, транслирует исходный код в машинный. Машинный код предназначен для выполнения на определенной модели процессора. Процессоры с общей архитектурой могут выполнять один и тот же код. Более поздние модели процессора как правило поддерживают инструкции предыдущих моделей, но не наоборот. Например, машинный код, использующий AVX инструкции процессоров Intel Sandy Bridge не может выполняться на более старых процессорах Intel. Существуют различные способы решения этой проблемы, например, вынесение критичных частей программы в библиотеку, имеющую версии под основные модели процессора. Но часто программы просто компилируются для относительно старых моделей процессоров и не используют преимущества новых наборов инструкций.
В противоположность компилируемым языкам программирования существуют интерпретируемые языки, такие как Perl и PHP. Один и тот же исходный код при таком подходе может быть запущен на любой платформе, для которой существует интерпретатор. Минусом этого подхода является то, что интерпретируемый код работает медленнее, чем машинный код, делающий тоже самое.
Язык Java предлагает другой подход, нечто среднее между компилируемыми и интерпретируемыми языками. Приложения на языке Java компилируются в промежуточный низкоуровневый код — байт-код (bytecode).
Название байт-код было выбрано потому, что для кодирования каждой операции используется ровно один байт. В Java 10 существует около 200 операций.
Байт-код затем исполняется JVM также как и программа на интерпретируемом языке. Но поскольку байт-код имеет строго определенный формат, JVM может компилировать его в машинный код прямо во время выполнения. Естественно, старые версии JVM не смогут сгенерировать машинный код, использующий новые наборы инструкций процессоров вышедших после них. С другой стороны, для того, чтобы ускорить Java-программу, ее даже не надо перекомпилировать. Достаточно запустить ее на более новой JVM.
HotSpot JIT компилятор
Единица скомпилированного кода называется nmethod (сокращение от native method).
Многоуровневая компиляция (tiered compilation)
На самом деле в HotSpot JVM существует не один, а два компилятора: C1 и C2. Другие их названия клиентский (client) и серверный (server). Исторически C1 использовался в GUI приложениях, а C2 в серверных. Отличаются компиляторы тем, как быстро они начинают компилировать код. C1 начинает компилировать код быстрее, в то время как C2 может генерировать более оптимизированный код.
Существует 5 уровней компиляции:
| Последовательность | Описание |
|---|---|
| 0-3-4 | Интерпретатор, уровень 3, уровень 4. Наиболее частый случай. |
| 0-2-3-4 | Случай, когда очередь уровня 4 (C2) переполнена. Код быстро компилируется на уровне 2. Как только профилирование этого кода завершится, он будет скомпилирован на уровне 3 и, наконец, на уровне 4. |
| 0-2-4 | Случай, когда очередь уровня 3 переполнена. Код может быть готов к компилированию на уровне 4 все еще ожидая своей очереди на уровне 3. Тогда он быстро компилируется на уровне 2 и затем на уровне 4. |
| 0-3-1 | Случай простых методов. Код сначала компилируется на уровне 3, где становится понятно, что метод очень простой и уровень 4 не сможет скомпилировать его оптимальней. Код компилируется на уровне 1. |
| 0-4 | Многоуровневая компиляция выключена. |
Code cache
Машинный код, скомпилированный JIT компилятором, хранится в области памяти называемой code cache. В ней также хранится машинный код самой виртуальной машины, например, код интерпретатора. Размер этой области памяти ограничен, и когда она заполняется, компиляция прекращается. В этом случае часть «горячих» методов так и продолжит выполняться интерпретатором. В случае переполнения JVM выводит следующее сообщение:
Другой способ узнать о переполнении этой области памяти — включить логирование работы компилятора (как это сделать обсуждается ниже).
Code cache настраивается также как и другие области памяти в JVM. Первоначальный размер задаётся параметром -XX:InitialCodeCacheSize. Максимальный размер задается параметром -XX:ReservedCodeCacheSize. По умолчанию начальный размер равен 2496 KB. Максимальный размер равен 48 MB при выключенной многоуровневой компиляции и 240 MB при включенной.
Начиная с Java 9 code cache разделен на 3 сегмента (суммарный размер по-прежнему ограничен пределами, описанными выше):
Мониторинг работы компилятора
Включить логирование процесса компиляции можно флагом -XX:+PrintCompilation (по умолчанию он выключен). При установке этого флага JVM будет выводить в стандартный поток вывода (STDOUT) сообщение каждый раз после компиляции метода или цикла. Большинство сообщений имеют следующий формат: timestamp compilation_id attributes tiered_level method_name size deopt.
Поле timestamp — это время со старта JVM.
Поле compilation_id — это внутренний ID задачи. Обычно он последовательно увеличивается в каждом сообщении, но иногда порядок может нарушаться. Это может произойти в случае, если существует несколько потоков компиляции работающих параллельно.
Поле attributes — это набор из пяти символов, несущих дополнительную информацию о скомпилированном коде. Если какой-то из атрибутов не применим, вместо него выводится пробел. Существуют следующие атрибуты:
Атрибут «b» означает, что компиляция произошла не в фоне, и не должен встречаться в современных версиях JVM.
Атрибут «n» означает, что скомпилированный метод является оберткой нативного метода.
Поле tiered_level содержит номер уровня, на котором был скомпилирован код или может быть пустым, если многоуровневая компиляция выключена.
Поле method_name содержит название скомпилированного метода или название метода, содержащего скомпилированный цикл.
Поле size содержит размер скомпилированного байт-кода, не размер полученного машинного кода. Размер указан в байтах.
Поле deopt появляется не в каждом сообщении, оно содержит название проведенной деоптимизации и может содержать такие сообщения как «made not entrant» и «made zombie».
Иногда в логе могут появиться записи вида: timestamp compile_id COMPILE SKIPPED: reason. Они означают, что при компиляции метода что-то пошло не так. Есть случаи, когда это ожидаемо:
Параметр -compiler выводит сводную информацию о работе компилятора (5003 — это ID процесса):
Эта команда также выводит количество методов, компиляция которых завершилась ошибкой и название последнего такого метода.
Планы на вторую часть
В следующей части мы рассмотрим пороговые значения счетчиков при которых JVM запускает компиляцию и как можно их поменять. Мы также рассмотрим как JVM выбирает количество потоков компилятора, как можно его поменять и в каких случаях стоит это делать. И наконец, кратко рассмотрим некоторые из оптимизаций выполняемых JIT компилятором.
Ахаха, HotSpot, что ты делаешь, прекрати!
Как вы наверняка уже знаете, скоро в Питере пройдёт очередная конференция Joker. Я собираюсь сделать на ней доклад о том, как расследовать поведение JVM, кажущееся поначалу таинственным и загадочным. Этот пост — тизер, предназначенный для того, чтобы дать вам возможность понять, чего ждать от доклада.
Предположим, что к вам вдруг приходит информация о проблеме: при сборке мусора отображаются причины «Last Ditch Collection» и «No GC», и поиск в интернете не даёт ничего вразумительного. К счастью, HotSpot практически полностью собирается из OpenJDK, и потому, как минимум в теории, мы можем найти ответы на все интересующие нас вопросы прямо в исходниках. Чем мы и займёмся!
Last Ditch Collection
Вооружившись исходным кодом OpenJDK, поищем интересующую нас строку:
Похоже, что это оно! Почитав метод, в котором находится вызов, и в особенности — его комментарии, мы довольно быстро понимаем, что происходит.
Звучит правдоподобно, но стоит научиться стабильно это воспроизводить. Если верить написанному выше, то при активном замусоривании metaspace мы должны увидеть как минимум одну сборку с причиной «Last ditch collection»:
Если это запустить с параметрами по умолчанию, то ждать придётся довольно долго. Но мы можем уменьшить размер metaspace и получить результат быстрее:
В большинстве случаев мы увидим более, чем одну сборку с причиной «last ditch collection». Это вполне ожидаемо, поскольку иначе бы от них не было толку, и вряд ли бы эту фичу вообще реализовали.
Кстати, здесь мы смотрели на исходники Java 9, но логика last ditch collection не менялась очень давно, так что тут беспокоиться не о чем.
No GC
Перейдём к «No GC». Капитан подсказывает нам, что разработчики HotSpot вряд ли бы назвали так нормальную причину сборки мусора. Да и вообще, единственный случай, в котором мы, по логике, должны такое увидеть — это если мы успеем запустить jstat раньше, чем произойдёт хоть одна сборка мусора:
Или если он сделает проверку, когда сборщик мусора не активен:
Так что, если это было обнаружено в другой ситуации, то это, должно быть, баг. А поскольку когда мы разбирали last ditch collection, таких странностей не наблюдалось, то есть шанс того, что он уже исправлен. Проявим немного оптимизма и поищем все подходящие коммиты:
Ага! Похоже, что проблему действительно пофиксили в коммите номер 2097, ещё в феврале 2013 года. Заглянув в файл hotspot_version, мы обнаруживаем, что последняя HotSpot, в которой должна наблюдаться проблема — это 21.0-b01. У меня под рукой была Java 6 с версией HotSpot 20.45-b01:
При запуске нашего примера с котиками, немедленно получается следующий результат:
Это довольно близко, но не совсем то, что мы искали. Однако, при более детальном изучении патча становится очевидным, что нужно просто использовать другой сборщик мусора. Добавление -XX:+UseG1GC даст нам нужный результат:
Успех! Кстати, тот человек, который первым ответит, почему тут какие-то знаки вопроса, и что именно должно навести нас на использование G1, получит приглашение на Joker Unconference 😉 SerCe уже дал правильный ответ.
Послесловие
Итак, только что, на основании одного-единственного твита и используя базовые утилиты командной строки, мы ответили на не гуглящийся вопрос, нашли стародавний баг в HotSpot, смогли благодаря этому оценить сверху версию JVM, и даже узнали, какой использовался сборщик мусора.
Инициализация и работа интерпретатора байткода в JVM HotSpot под x86
Почти каждый Java разработчик знает, что программы, написанные на языке Java изначально компилируются в JVM-байткод и хранятся в виде class-файлов стандартизованного формата. После попадания таких class-файлов внутрь виртуальной машины и пока до них еще не успел добраться компилятор, JVM интерпретирует байткод, содержащийся в этих class-файлах. Данная статься содержит обзор принципов работы интерпретатора применительно к OpenJDK JVM HotSpot.
Окружение
Для экспериментов используется сборка крайней доступной ревизии 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 разбирает аргументы, переданные из командной строки. Этот парсер аргументов определяет режим запуска приложения
III. Форк primordial потока и создание в нем VM
Но если что-то пошло не так, то делается попытка запустить JVM в main-треде «just give it a try».
Итак, будем считать, что на данный момент у нас успешно распарсились опции и создался поток для VM. После этого, только что форкнутый поток начинает создание виртуальной машины и попадает в функцию Threads::create_vm
В этой функции делается довольно большое количество черной магии инициализаций, нам интересны будут лишь некоторые из них.
Инициализация интепретатора и передача управления java-коду
Чтобы обеспечить работу данной машинерии в Threads::create_vm выполняется ряд инициализаций, от которых зависит интерпретатор:
I. Инициализация таблицы доступных байткодов
Прежде чем приступить к инициализации интерпретатора, необходимо проинициализировать таблицу используемых байткодов. Она выполняется в функции Bytecodes::initialize и представлена в виде очень удобочитаемой таблички. Ее фрагмент выглядит следующим образом:
Эти параметры в дальнейшем нужны для генерации кода шаблонов интерпретатора
II. Инициализация код кэша
Для того, чтобы сгенерить код шаблонов интерпретатора, необходимо сперва выделить под это дело память. Резервация памяти под код кэш реализована в функции с одноименным названием CodeCache::initialize(). Как можно видеть из следущего участка кода данной функции
После выполнения проверок резервируется область размером в ReservedCodeCacheSize байт. В случае, если SegmentedCodeCache оказалась выставленной, то данная область разбивается на части: JIT-скомпилированные методы, стаб рутины, и т.д.
III. Инициализация шаблонов интерпретатора
После того, как таблица байткодов и код кэш проинициализированы, можно приступать к кодогенерации шаблонов интерпретатора. Для этого интерепретатор резервирует буффер из ранее проинициализированного код кэша. На каждый этап кодогенерации из буффера будут отрезаться кодлеты — небольшие участки кода. После завершению текущей генерации, неиспользуемая под код часть кодлета освобождается и становится доступной для последующих кодогенераций.
Рассмотрим каждый из этих этапов по отдельности:
signature handler используется для подготовки аргументов для вызовов нативных методов. В данном случае генерится обощенный хэндлер, если, например у нативного метода больше 13 аргументов (В дебаггере не проверял, но судя по коду должны быть так)
VM валидирует классфайлы при инициализации, но это на случай, если аргументы на стеке не того формата который нужен или байткод о котором VM не знает. Эти стабы используются при генерации кода шаблонов для каждого из байткодов.
После вызова процедур необходимо восстановить данные стек фрейма, который был до вызова процедуры из которой делается return.
Используется при вызовах рантайма из интерепретатора.
Представлен в виде макроса в зависимости от типа метода. В общем случае выполняется подготовка интерпретируемого стек-фрейма, проверка StackOverflow, stack-banging. Для нативных методов определяется signature handler.
Для выполнения инструкции спецификация VM требует чтобы операнды находились в Operand Stack, но это не запрещает HotSpot кэшировать их в регистре. Для определения текущего состояния вершины стека используется перечисление
Каждая инструкция определяет входные и выходные состояния TosState вершины стека, и генерация шаблонов происходит в зависимости от этого состояния. Данные шаблоны инициализируются в удобочитаемой таблице шаблонов. Фрагмент этой таблицы выглядит следующим образом:
in — состояние вершины стека на момент начала исполнения инструкции
out — состояния вершины стека на момент завершения исполнения инструкции
generator — генератор шаблона машинного кода инструкции
Общий вид шаблона для всех байткодов можно описать в виде:
Если для инструкции не выставлен dispatch bit, то выполняется пролог инструкции (no-op на x86)
Если для инструкции не выставлен dispatch bit, то выполняется переход к следующей по порядку инструкции в зависимости от out состояния вершины стека, которое будет являтся in для следующей инструкции
Адрес точки входа для полученного шаблона сохраняется в глобальной таблице и его можно использовать при отладке.
В HotSpot за это отвечает следущий, относительно стремный кусок кода:
Как только данная кодогенерация завершена, интепретатор можно считать полностью проинициализированным. После интерпретатора выполняется еще много инициализаций различных подсистем JVM. Для некоторых из них требуется вызывать Java-код из кода виртуальной машины. Это реализовано с помощью стандартного механизма JavaCalls. После того как инициализация JVM полностью завершена, этот механизм используется для вызова метода main.
Пример
Для того, чтобы представлять как это все работает на практике, рассмотрим следующий относительно простой пример:
Байткод ровно такой, какой нам нужен и первое с чего придется начать — это с анализа вызова статического метода.
Генератор шаблона invokestatic ‘а для x86 находится в архитектурно-зависимой секции кода HotSpot и представлен в виде
Ну а теперь и сама инструкция iadd :
Если посмотреть в gdb на eax и edx сразу перед выполнением сложения, то можно заметить






