Ответ 1
Java требует, чтобы все классы, которые были загружены для проверки, сохраняли безопасность изолированной программной среды и гарантировали безопасность кода для оптимизации. Обратите внимание, что это делается на уровне байт-кода, поэтому проверка не проверяет инварианты языка Java, она просто проверяет, что байт-код имеет смысл в соответствии с правилами для байт-кода.
Кроме того, проверка байт-кода вещей гарантирует, что инструкции хорошо сформированы, что все прыжки являются действительными инструкциями внутри метода и что все команды работают с значениями правильного типа. Последний - это место, куда входит карта стека.
Дело в том, что байт-код сам по себе не содержит информации о явном типе. Типы определяются неявно посредством анализа потока данных. Например, инструкция iconst создает целочисленное значение. Если вы храните его в слоте 1, этот слот теперь имеет int. Если поток управления сливается из кода, в котором вместо этого хранится float, в слоте теперь считается недопустимый тип, что означает, что вы не можете ничего сделать с этим значением до перезаписывания.
Исторически, верификатор байт-кода вывел все типы, используя эти правила потока данных. К сожалению, невозможно сделать вывод о всех типах в одном линейном проходе через байт-код, потому что обратный прыжок может привести к недействительности уже выведенных типов. Классический верификатор решил это путем повторения кода, пока все не перестало меняться, что, возможно, потребовало нескольких проходов.
Однако проверка замедляет загрузку классов в Java. Oracle решила решить эту проблему, добавив новый, более быстрый верификатор, который может проверять байт-код за один проход. Для этого им потребовались все новые классы, начиная с Java 7 (с Java 6 в переходном состоянии) для переноса метаданных об их типах, чтобы байт-код можно было проверить за один проход. Поскольку сам формат байт-кода не может быть изменен, информация этого типа хранится отдельно в атрибуте StackMapTable
.
Простое сохранение типа для каждого отдельного значения в каждой отдельной точке кода, очевидно, займет много места и будет очень расточительным. Чтобы сделать метаданные более компактными и эффективными, они решили, что они отображают только типы в позициях, которые являются мишенями прыжков. Если вы думаете об этом, это единственный раз, когда вам нужна дополнительная информация для проверки одного прохода. В промежутке между целевыми мишенями весь поток управления является линейным, поэтому вы можете вывести типы между позициями, используя старые правила вывода.
Каждая позиция, в которой типы явно указаны, называется фреймом карты стека. Атрибут StackMapTable
содержит список кадров в порядке, хотя они обычно выражаются как разница с предыдущим кадром, чтобы уменьшить размер данных. Если в методе нет фреймов, который возникает, когда поток управления никогда не соединяется (т.е. CFG - это дерево), тогда атрибут StackMapTable может быть полностью опущен.
Итак, это основная идея работы StackMapTable и почему она была добавлена. Последний вопрос заключается в том, как создается неявный начальный кадр. Разумеется, ответ заключается в том, что в начале метода стек операндов пуст, а локальные слоты переменных имеют типы, заданные типами параметров метода, которые определяются из метода decriptor.
Если вы привыкли к Java, есть несколько незначительных отличий от того, как типы параметров метода работают на уровне байт-кода. Во-первых, виртуальные методы имеют неявный this
в качестве первого параметра. Во-вторых, boolean
, byte
, char
и short
не существуют на уровне байт-кода. Вместо этого все они реализованы как ints за кулисами.