Глава 2. Структура виртуальной машины Java
Оглавление
- 2.1. Формат файла
class - 2.2. Типы данных
- 2.3. Примитивные типы и значения
- 2.4. Ссылочные типы и значения
- 2.5. Области данных во время выполнения
- 2.6. Фреймы
- 2.7. Представление объектов
- 2.8. Арифметика с плавающей точкой
- 2.9. Специальные методы
- 2.10. Исключения
- 2.11. Краткий обзор набора команд
-
- 2.11.1. Типы и виртуальная машина Java
- 2.11.2. Команды загрузки и сохранения
- 2.11.3. Арифметические команды
- 2.11.4. Команды преобразования типов
- 2.11.5. Создание и обработка объектов
- 2.11.6. Команды управления стеком операндов
- 2.11.7. Команды передачи управления
- 2.11.8. Команды вызова и возврата методов
- 2.11.9. Генерация исключений
- 2.11.10. Синхронизация
- 2.12. Библиотеки классов
- 2.13. Публичный дизайн, приватная реализация
Этот документ описывает абстрактную машину. Он не описывает никакой конкретной реализации виртуальной машины Java.
Для корректной реализации виртуальной машины Java вам необходимо только уметь читать формат файла class и правильно выполнять в нём указанные операции. Детали реализации, которые не являются частью спецификации виртуальной машины Java, излишне ограничат творческий потенциал разработчиков. Например, расположение в памяти областей данных во время выполнения, используемый алгоритм сборки мусора и любые внутренние оптимизации команд виртуальной машины Java (например, их перевод в машинный код) оставляются на усмотрение разработчика.
Все ссылки на Unicode в этой спецификации даются относительно Стандарта Unicode, версия 6.0.0, доступного по адресу http://www.unicode.org/.
Скомпилированный код, который должен выполняться виртуальной машиной Java, представлен в формате, независимом от аппаратной платформы и операционной системы, обычно (но необязательно) хранящемся в файле, известном как формат файла class. Формат файла class точно определяет представление класса или интерфейса, включая такие детали, как порядок байтов, которые могут быть приняты как само собой разумеющиеся в формате платформенно-зависимого объектного файла.
Глава 4, "Формат файла class", подробно рассматривает формат файла class.
Подобно языку программирования Java, виртуальная машина Java работает с двумя видами типов: примитивными типами и ссылочными типами. Соответственно, есть два вида значений, которые могут храниться в переменных, передаваться в качестве аргументов, возвращаться методами и обрабатываться: примитивные значения и ссылочные значения.
Виртуальная машина Java ожидает, что почти вся проверка типов выполняется до времени выполнения, обычно компилятором, и не должна выполняться самой виртуальной машиной. Значения примитивных типов не должны быть помечены или каким-либо другим образом инспектируемы, чтобы определить их типы во время выполнения или отличить их от значений ссылочных типов. Вместо этого набор команд виртуальной машины Java различает типы операндов, используя команды, предназначенные для работы со значениями определённых типов. Например, iadd, ladd, fadd и dadd — это команды виртуальной машины Java, которые складывают два числовых значения и производят числовые результаты, но каждая из них специализируется на своём типе операндов: int, long, float и double соответственно. Сводку поддержки типов в наборе команд виртуальной машины Java см. в §2.11.1.
Виртуальная машина Java содержит явную поддержку объектов. Объект — это либо динамически выделенный экземпляр класса, либо массив. Ссылка на объект рассматривается как имеющая тип виртуальной машины Java reference. Значения типа reference можно рассматривать как указатели на объекты. Может существовать более одной ссылки на объект. Объекты всегда обрабатываются, передаются и проверяются с помощью значений типа reference.
Примитивные типы данных, поддерживаемые виртуальной машиной Java, — это числовые типы, тип boolean (§2.3.4) и тип returnAddress (§2.3.3).
Числовые типы состоят из целочисленных типов (§2.3.1) и типов с плавающей точкой (§2.3.2).
Целочисленные типы:
-
byte, значения которого представляют собой 8-битные целые числа со знаком в дополнительном коде, а значение по умолчанию равно нулю -
short, значения которого представляют собой 16-битные целые числа со знаком в дополнительном коде, а значение по умолчанию равно нулю -
int, значения которого представляют собой 32-битные целые числа со знаком в дополнительном коде, а значение по умолчанию равно нулю -
long, значения которого представляют собой 64-битные целые числа со знаком в дополнительном коде, а значение по умолчанию равно нулю -
char, значения которого представляют собой 16-битные беззнаковые целые числа, кодирующие точки кода Юникода в базовой многоязычной плоскости, закодированные в UTF-16, и значение по умолчанию — нулевая точка кода ('\u0000')
Типы с плавающей точкой:
-
float, значения которого — элементы множества значений float или, если поддерживается, множества значений float с расширенным порядком экспоненты, а значение по умолчанию — положительный ноль -
double, значения которого — элементы множества значений double или, если поддерживается, множества значений double с расширенным порядком экспоненты, а значение по умолчанию — положительный ноль
Значения типа boolean кодируют логические значения true и false, а значение по умолчанию — false.
В первом издании спецификации виртуальной машины Java The Java® Virtual Machine Specification тип boolean не рассматривался как тип виртуальной машины Java. Однако значения boolean имеют ограниченную поддержку в виртуальной машине Java. Во втором издании спецификации было уточнено, что boolean является типом.
Значения типа returnAddress — указатели на коды операций инструкций виртуальной машины Java. Из примитивных типов только тип returnAddress напрямую не связан с типом языка программирования Java.
Значения целочисленных типов виртуальной машины Java:
-
Для
byte, от -128 до 127 включительно (-27 до 27 - 1) -
Для
short, от -32768 до 32767 включительно (-215 до 215 - 1) -
Для
int, от -2147483648 до 2147483647 включительно (-231 до 231 - 1) -
Для
long, от -9223372036854775808 до 9223372036854775807 включительно (-263 до 263 - 1) -
Для
char, от 0 до 65535 включительно
Типы с плавающей точкой — это float и double, которые концептуально связаны со значениями и операциями формата IEEE 754 с одинарной точностью (32 бита) и двойной точностью (64 бита), как определено в стандарте IEEE Standard for Binary Floating-Point Arithmetic (ANSI/IEEE Std. 754-1985, Нью-Йорк).
Стандарт IEEE 754 включает не только положительные и отрицательные числа со знаком и величиной, но также положительные и отрицательные нули, положительные и отрицательные бесконечности и специальное значение «не число» (в дальнейшем сокращенно как «NaN»). Значение NaN используется для представления результата некоторых недопустимых операций, таких как деление нуля на ноль.
Каждая реализация Java Virtual Machine обязана поддерживать два стандартных набора значений с плавающей точкой, называемые множеством значений с плавающей точкой типа float и множеством значений с плавающей точкой типа double. Кроме того, реализация Java Virtual Machine может по своему усмотрению поддерживать один или оба расширенных множества значений с плавающей точкой с расширенным порядком, называемые множеством значений с плавающей точкой типа float с расширенным порядком и множеством значений с плавающей точкой типа double с расширенным порядком. Эти множества значений с расширенным порядком могут в определенных случаях использоваться вместо стандартных множеств значений для представления значений типа float или double.
Все конечные отличные от нуля значения любого множества значений с плавающей точкой могут быть выражены в виде s ⋅ m ⋅ 2(e − N + 1), где s равно +1 или −1, m — положительное целое число, меньшее 2N, а e — целое число от Emin = −(2K−1−2) до Emax = 2K−1−1 включительно, и где N и K — параметры, зависящие от множества значений. Некоторые значения могут быть представлены в этом виде более чем одним способом; например, предположим, что значение v в множестве значений может быть представлено в этом виде с определенными значениями s, m и e, затем, если m является чётным и e меньше 2K-1, можно разделить m на 2 и увеличить e на 1, чтобы получить второе представление того же значения v. Представление в этом виде называется нормализованным, если m ≥ 2N-1; в противном случае представление называется денормализованным. Если значение в множестве значений нельзя представить таким образом, что m ≥ 2N-1, то это значение называется денормализованным значением, поскольку у него нет нормализованного представления.
Ограничения на параметры N и K (и на производные параметры Emin и Emax) для двух требуемых и двух необязательных множеств значений с плавающей точкой приведены в таблице 2.3.2-А.
Таблица 2.3.2-А. Параметры множества значений с плавающей точкой
| Параметр | float | float-extended-exponent | double | double-extended-exponent |
|---|---|---|---|---|
| N | 24 | 24 | 53 | 53 |
| K | 8 | ≥ 11 | 11 | ≥ 15 |
| Emax | +127 | ≥ +1023 | +1023 | ≥ +16383 |
| Emin | -126 | ≤ -1022 | -1022 | ≤ -16382 |
Если реализация поддерживает одно или оба множества значений с расширенным порядком, то для каждого поддерживаемого множества значений с расширенным порядком существует определенная зависящая от реализации константа K, значение которой ограничено таблицей 2.3.2-А; это значение K, в свою очередь, определяет значения Emin и Emax.
Каждое из четырёх множеств значений включает не только конечные отличные от нуля значения, описанные выше, но также пять значений: положительный ноль, отрицательный ноль, положительная бесконечность, отрицательная бесконечность и NaN.
Обратите внимание, что ограничения в таблице 2.3.2-А разработаны таким образом, чтобы каждый элемент множества значений типа float также был элементом множества значений типа float с расширенным порядком, множества значений типа double и множества значений типа double с расширенным порядком. Аналогично, каждый элемент множества значений типа double также является элементом множества значений типа double с расширенным порядком. Каждое множество значений с расширенным порядком имеет больший диапазон значений порядка, чем соответствующее стандартное множество значений, но не имеет большей точности.
Элементы множества значений типа float — это именно те значения, которые могут быть представлены с помощью формата с одинарной плавающей точкой, определенного в стандарте IEEE 754, за исключением того, что существует только одно значение NaN (стандарт IEEE 754 определяет 224-2 различных значений NaN). Элементы множества значений типа double — это именно те значения, которые могут быть представлены с помощью формата с двойной плавающей точкой, определенного в стандарте IEEE 754, за исключением того, что существует только одно значение NaN (стандарт IEEE 754 определяет 253-2 различных значений NaN). Однако следует отметить, что элементы множеств значений типа float с расширенным порядком и double с расширенным порядком, определенные здесь, не соответствуют значениям, которые могут быть представлены с помощью расширенных форматов с одинарной и двойной плавающей точкой IEEE 754 соответственно. Этот стандарт не предписывает определенного представления для значений множеств значений с плавающей точкой, за исключением случаев, когда значения с плавающей точкой должны быть представлены в формате файла class (§4.4.4, §4.4.5).
Множества значений типа float, float с расширенным порядком, double и double с расширенным порядком не являются типами. Для реализации Java Virtual Machine всегда корректно использовать элемент множества значений типа float для представления значения типа float; однако в определенных контекстах реализация может использовать элемент множества значений типа float с расширенным порядком. Аналогично, для реализации всегда корректно использовать элемент множества значений типа double для представления значения типа double; однако в определенных контекстах реализация может использовать элемент множества значений типа double с расширенным порядком.
За исключением NaN, значения множеств значений с плавающей точкой являются упорядоченными. При упорядочении от наименьшего к наибольшему они представляют собой отрицательную бесконечность, отрицательные конечные значения, положительный и отрицательный ноль, положительные конечные значения и положительную бесконечность.
Положительный ноль с плавающей точкой и отрицательный ноль с плавающей точкой сравниваются как равные, но есть другие операции, которые могут их различать; например, деление 1.0 на 0.0 даёт положительную бесконечность, а деление 1.0 на -0.0 даёт отрицательную бесконечность.
NaN не упорядочены, поэтому численные сравнения и проверки на численное равенство имеют значение false, если один или оба операнда равны NaN. В частности, проверка численного равенства значения самому себе имеет значение false тогда и только тогда, когда значение равно NaN. Проверка численного неравенства имеет значение true, если один из операндов равен NaN.
Тип returnAddress используется инструкциями Java Virtual Machine jsr, ret и jsr_w (§jsr, §ret, §jsr_w). Значения типа returnAddress — это указатели на операторы инструкций Java Virtual Machine. В отличие от числовых примитивных типов, тип returnAddress не соответствует никакому типу языка программирования Java и не может быть изменён работающей программой.
Хотя виртуальная машина Java определяет тип boolean, она предоставляет лишь очень ограниченную поддержку для него. Нет инструкций виртуальной машины Java, посвященных исключительно операциям над значениями boolean. Вместо этого выражения на языке программирования Java, работающие со значениями boolean, компилируются с использованием значений типа данных int виртуальной машины Java.
Виртуальная машина Java напрямую поддерживает массивы типа boolean. Инструкция newarray (§newarray) позволяет создавать массивы boolean. Массивы типа boolean обращаются к ним и изменяют их с помощью инструкций для массивов byte baload и bastore (§baload, §bastore).
В реализации виртуальной машины Java от Oracle, массивы boolean на языке программирования Java закодированы как массивы виртуальной машины byte, используя 8 бит на каждый элемент boolean.
Виртуальная машина Java кодирует компоненты массива boolean, используя 1 для представления true и 0 для представления false. Где компиляторы языка программирования Java отображают значения boolean на значения типа виртуальной машины int, компиляторы должны использовать то же кодирование.
Существует три вида типов reference: типы классов, типы массивов и типы интерфейсов. Их значения являются ссылками на динамически созданные экземпляры классов, массивы или экземпляры классов или массивы, которые реализуют интерфейсы соответственно.
Тип массива состоит из типа компонента с одним измерением (длина которого не указана типом). Тип компонента типа массива сам может быть типом массива. Если, начиная с любого типа массива, рассматривать его тип компонента, а затем (если это также тип массива) тип компонента этого типа и так далее, в конечном итоге необходимо достичь типа компонента, который не является типом массива; это называется типом элемента типа массива. Тип элемента типа массива обязательно является либо примитивным типом, либо типом класса, либо типом интерфейса.
Значение типа reference также может быть специальной ссылкой null, ссылкой на ни один объект, которая здесь будет обозначаться как null. Ссылка null изначально не имеет типа во время выполнения, но может быть приведена к любому типу. Значение по умолчанию для типа reference — null.
Этот документ не накладывает требования к конкретной кодировке значений null.
Виртуальная машина Java определяет различные области данных во время выполнения, которые используются во время выполнения программы. Некоторые из этих областей данных создаются при запуске виртуальной машины Java и уничтожаются только при завершении работы виртуальной машины Java. Другие области данных относятся к каждому потоку. Области данных, относящиеся к каждому потоку, создаются при создании потока и уничтожаются при завершении работы потока.
Виртуальная машина Java может поддерживать множество потоков выполнения одновременно (JLS §17). Каждый поток виртуальной машины Java имеет свой собственный регистр pc (счётчик команд). В любой момент каждый поток виртуальной машины Java выполняет код одного метода, а именно текущего метода (§2.6) для данного потока. Если этот метод не native, то регистр pc содержит адрес инструкции виртуальной машины, которая в данный момент выполняется. Если метод, который в данный момент выполняет поток, является native, то значение регистра pc виртуальной машины не определено. Регистр pc виртуальной машины достаточно широк, чтобы содержать returnAddress или указатель нативные на конкретной платформе.
Каждый поток виртуальной машины Java имеет свой собственный частный стек виртуальной машины Java, созданный одновременно с потоком. Стек виртуальной машины Java хранит кадры (§2.6). Стек виртуальной машины Java аналогичен стеку традиционного языка, такого как C: он хранит локальные переменные и промежуточные результаты, а также участвует в вызовах и возврате методов. Поскольку стек виртуальной машины Java никогда не манипулируется напрямую, кроме как для помещения и извлечения кадров, кадры могут быть размещены в куче. Память для стека виртуальной машины Java не обязана быть непрерывной.
В первом издании спецификации Виртуальной машины Java® стек виртуальной машины Java был известен как стек Java.
Данная спецификация разрешает стекам виртуальной машины Java быть либо фиксированного размера, либо динамически расширяться и сжиматься по мере необходимости вычислением. Если стеки виртуальной машины Java имеют фиксированный размер, размер каждого стека виртуальной машины Java может быть выбран независимо при создании этого стека.
Реализация виртуальной машины Java может предоставить программисту или пользователю управление начальным размером стеков виртуальной машины Java, а также, в случае динамического расширения или сжатия стеков виртуальной машины Java, управление максимальным и минимальным размерами.
Следующие исключительные ситуации связаны со стеком виртуальной машины Java:
-
Если вычисления в потоке требуют стека виртуальной машины Java больше, чем разрешено, виртуальная машина Java генерирует исключение
StackOverflowError. -
Если стеки виртуальной машины Java могут динамически расширяться, и попытка расширения, но доступно недостаточно памяти для осуществления расширения, или если недостаточно памяти для создания начального стека виртуальной машины Java для нового потока, виртуальная машина Java генерирует исключение
OutOfMemoryError.
Виртуальная машина Java имеет кучу, которая используется всеми потоками виртуальной машины Java. Куча — это область данных во время выполнения, из которой выделяется память для всех экземпляров классов и массивов.
Куча создается при запуске виртуальной машины. Хранение объектов в куче освобождается системой автоматического управления памятью (известной как мусорный сборщик); объекты никогда не освобождаются явно. Виртуальная машина Java не предполагает какой-либо определённой системы автоматического управления памятью, и метод управления памятью может быть выбран в соответствии с потребностями системы реализатора. Куча может быть фиксированного размера или расширяться по мере необходимости вычисления и может сжиматься, если большая куча становится ненужной. Память для кучи не обязана быть непрерывной.
Реализация виртуальной машины Java может предоставить программисту или пользователю управление начальным размером кучи, а также, если куча может динамически расширяться или сжиматься, управление максимальным и минимальным размером кучи.
Следующее исключительное условие связано с кучей:
-
Если вычисление требует больше кучи, чем может предоставить система автоматического управления памятью, виртуальная машина Java генерирует исключение
OutOfMemoryError.
Виртуальная машина Java имеет область методов, которая используется всеми потоками виртуальной машины Java. Область методов аналогична области хранения скомпилированного кода традиционного языка или аналогична сегменту «текст» в процессе операционной системы. Она хранит структуры на основе классов, такие как постоянный пул времени выполнения, данные полей и методов, а также код методов и конструкторов, включая специальные методы (§2.9), используемые при инициализации классов и экземпляров и инициализации интерфейсов.
Область методов создаётся при запуске виртуальной машины. Несмотря на то, что область методов логически является частью кучи, простые реализации могут выбрать не выполнять сборку мусора или уплотнение. Данная спецификация не предписывает расположение области методов или стратегии управления скомпилированным кодом. Область методов может быть фиксированного размера или расширяться по мере необходимости вычисления и может сжиматься, если большая область методов становится ненужной. Память для области методов не обязана быть непрерывной.
Реализация виртуальной машины Java может предоставить программисту или пользователю управление начальным размером области методов, а также, в случае области методов переменного размера, управление максимальным и минимальным размером области методов.
Следующее исключительное условие связано с областью методов:
-
Если память в области методов недоступна для удовлетворения запроса на выделение памяти, виртуальная машина Java генерирует исключение
OutOfMemoryError.
Постоянный пул времени выполнения — это представление во время выполнения на основе класса или интерфейса таблицы constant_pool в файле class (§4.4). Он содержит несколько типов констант, начиная от числовых литералов, известных на этапе компиляции, до ссылок на методы и поля, которые должны быть разрешены во время выполнения. Постоянный пул времени выполнения выполняет функцию, аналогичную функции таблицы символов для традиционного языка программирования, хотя он содержит более широкий диапазон данных, чем типичная таблица символов.
Каждый постоянный пул времени выполнения выделяется из области методов виртуальной машины Java (§2.5.4). Постоянный пул времени выполнения для класса или интерфейса создаётся при создании класса или интерфейса (§5.3) виртуальной машиной Java.
Следующее исключительное условие связано с созданием постоянного пула времени выполнения для класса или интерфейса:
-
При создании класса или интерфейса, если для создания постоянного пула времени выполнения требуется больше памяти, чем доступно в области методов виртуальной машины Java, виртуальная машина Java генерирует исключение
OutOfMemoryError.
См. §5 (Загрузка, связывание и инициализация) для получения информации о создании постоянного пула времени выполнения.
Реализация Java Virtual Machine может использовать обычные стеки, разговорно называемые "C-стеками", для поддержки native методов (методы, написанные на языке, отличном от языка программирования Java). Стеки нативных методов также могут использоваться реализацией интерпретатора набора инструкций Java Virtual Machine на языке, таком как C. Реализации Java Virtual Machine, которые не могут загрузить native методы и не полагаются сами на обычные стеки, не обязаны предоставлять стеки нативных методов. Если они предоставляются, стеки нативных методов обычно выделяются на каждый поток при создании каждого потока.
Этот спецификация допускает, что стеки нативных методов могут быть либо фиксированного размера, либо динамически расширяться и сжиматься по мере необходимости вычисления. Если стеки нативных методов имеют фиксированный размер, размер каждого стека нативных методов может выбираться независимо при создании этого стека.
Реализация Java Virtual Machine может предоставить программисту или пользователю контроль над начальным размером стеков нативных методов, а также, в случае стеков нативных методов переменного размера, контроль над максимальным и минимальным размерами стека методов.
Следующие исключительные ситуации связаны со стеками нативных методов:
-
Если вычисления в потоке требуют стека нативных методов большего размера, чем разрешено, Java Virtual Machine выбрасывает исключение
StackOverflowError. -
Если стеки нативных методов могут быть динамически расширены, а попытка расширения стека нативных методов, но недостаточно памяти может быть выделена, или если недостаточно памяти может быть выделено для создания начального стека нативных методов для нового потока, Java Virtual Machine выбрасывает исключение
OutOfMemoryError.
Фрейм используется для хранения данных и промежуточных результатов, а также для выполнения динамической связи, возврата значений для методов и обработки исключений.
Каждый раз при вызове метода создается новый фрейм. Фрейм уничтожается при завершении вызова его метода, будь то нормальное завершение или внезапное (выбрасывание необработанного исключения). Фреймы выделяются из стека виртуальной машины Java (§2.5.2) потока, создавшего фрейм. Каждый фрейм имеет свой массив локальных переменных (§2.6.1), свой операционный стек (§2.6.2) и ссылку на пул постоянных значений выполнения (§2.5.5) класса текущего метода.
Фрейм может быть расширен дополнительной информацией, специфичной для реализации, например, информацией об отладке.
Размеры массива локальных переменных и операционного стека определяются во время компиляции и предоставляются вместе с кодом метода, связанного с фреймом (§4.7.3). Таким образом, размер структуры данных фрейма зависит только от реализации виртуальной машины Java, и память для этих структур может быть выделена одновременно при вызове метода.
В любой момент в заданном потоке управления активен только один фрейм — фрейм для выполняемого метода. Этот фрейм называется текущим фреймом, а его метод — текущим методом. Класс, в котором определен текущий метод, является текущим классом. Операции с локальными переменными и операционным стеком обычно выполняются по отношению к текущему фрейму.
Фрейм перестаёт быть текущим, если его метод вызывает другой метод или если его метод завершается. Когда метод вызывается, создаётся новый фрейм, который становится текущим при передаче управления новому методу. При возврате из метода текущий фрейм возвращает результат вызова его метода, если таковой имеется, предыдущему фрейму. Затем текущий фрейм удаляется, и предыдущий фрейм становится текущим.
Обратите внимание, что фрейм, созданный потоком, является локальным для этого потока и не может быть получен никаким другим потоком.
Каждый фрейм (§2.6) содержит массив переменных, известный как его локальные переменные. Длина массива локальных переменных фрейма определяется во время компиляции и предоставляется в двоичном представлении класса или интерфейса вместе с кодом метода, связанного с фреймом (§4.7.3).
Одна локальная переменная может содержать значение типа boolean, byte, char, short, int, float, reference или returnAddress. Пара локальных переменных может содержать значение типа long или double.
Локальные переменные адресуются по индексу. Индекс первой локальной переменной равен нулю. Целое число рассматривается как индекс в массиве локальных переменных только в том случае, если это целое число находится между нулём и размером массива локальных переменных минус единица.
Значение типа long или типа double занимает две последовательные локальные переменные. Такое значение может быть адресовано только с использованием меньшего индекса. Например, значение типа double, хранящееся в массиве локальных переменных по индексу n, фактически занимает локальные переменные с индексами n и n+1; однако, локальную переменную по индексу n+1 нельзя загружать. Она может быть записана. Однако при этом аннулируется содержимое локальной переменной n.
Виртуальная машина Java не требует, чтобы n было чётным. Проще говоря, значения типов long и double не обязаны быть выровнены по 64 битам в массиве локальных переменных. Реализаторы могут самостоятельно выбрать подходящий способ представления таких значений, используя две локальные переменные, зарезервированные для значения.
Виртуальная машина Java использует локальные переменные для передачи параметров при вызове метода. При вызове метода класса любые параметры передаются в последовательные локальные переменные, начиная с локальной переменной 0. При вызове метода экземпляра локальная переменная 0 всегда используется для передачи ссылки на объект, к которому применяется метод экземпляра (this на языке программирования Java). Любые параметры затем передаются в последовательные локальные переменные, начиная с локальной переменной 1.
Каждый фрейм (§2.6) содержит стек, работающий по принципу «последним вошел — первым вышел» (LIFO), известный как его операторный стек. Максимальная глубина операционного стека фрейма определяется во время компиляции и предоставляется вместе с кодом метода, связанного с фреймом (§4.7.3).
Когда контекст понятен, мы иногда будем называть операционный стек текущего фрейма просто операционным стеком.
Операторный стек пуст, когда фрейм, который его содержит, создан. Виртуальная машина Java предоставляет инструкции для загрузки констант или значений из локальных переменных или полей в операционный стек. Другие инструкции виртуальной машины Java берут операнды из операционного стека, выполняют операции над ними и помещают результат обратно в операционный стек. Операторный стек также используется для подготовки параметров для передачи методам и получения результатов методов.
Например, инструкция iadd (§iadd) складывает два значения int. Она требует, чтобы два значения int, которые нужно сложить, были двумя верхними значениями операционного стека, помещёнными туда предыдущими инструкциями. Оба значения int извлекаются из операционного стека. Они складываются, и их сумма помещается обратно в операционный стек. Подвычисления могут быть вложены в операционном стеке, что приводит к значениям, которые могут использоваться в охватывающем вычислении.
Каждый элемент операционного стека может содержать значение любого типа виртуальной машины Java, включая значение типа long или типа double.
Значения из операционного стека должны обрабатываться соответствующим их типам образом. Например, нельзя поместить два значения int и затем обрабатывать их как long, или поместить два значения float и затем складывать их с инструкцией iadd. Небольшое количество инструкций виртуальной машины Java (инструкции dup (§dup) и swap (§swap)) оперируют областями данных во время выполнения как сырыми значениями, не учитывая их конкретные типы; эти инструкции определены таким образом, что они не могут использоваться для изменения или разделения отдельных значений. Эти ограничения на манипуляции операционным стеком контролируются проверкой файлов class (§4.10).
В любой момент времени у операционного стека есть связанная глубина, где значение типа long или double вносит два единицы в глубину, а значение любого другого типа вносит одну единицу.
Каждый фрейм (§2.6) содержит ссылку на пул постоянных значений выполнения (§2.5.5) для типа текущего метода для поддержки динамической ссылки на код метода. Код метода файла class ссылается на вызываемые методы и переменные через символические ссылки. Динамическая ссылка преобразует эти символические ссылки на метод в конкретные ссылки на метод, загружая классы по мере необходимости для разрешения пока ещё неопределённых символов и преобразует обращения к переменным в соответствующие смещения в структурах хранения, связанных с местоположением этих переменных во время выполнения.
Эта поздняя привязка методов и переменных делает менее вероятным, что изменения в других классах, которые использует метод, повредят этот код.
Вызов метода завершается нормально, если этот вызов не вызывает исключение (§2.10), которое выбрасывается либо напрямую из виртуальной машины Java, либо в результате выполнения явного throw оператора. Если вызов текущего метода завершается нормально, то вызываемому методу может быть возвращено значение. Это происходит, когда вызываемый метод выполняет одну из инструкций возврата (§2.11.8), выбор которой должен быть соответствующим типу возвращаемого значения (если оно есть).
В этом случае текущий фрейм (§2.6) используется для восстановления состояния вызывающего метода, включая его локальные переменные и стек операндов, при этом счётчик команд вызывающего метода увеличивается соответствующим образом, чтобы перейти за инструкцию вызова метода. Затем выполнение продолжается нормально в фрейме вызывающего метода со возвращаемым значением (если оно есть), помещённым в стек операндов этого фрейма.
Вызов метода завершается с прерыванием, если выполнение инструкции виртуальной машины Java внутри метода вызывает бросок исключения (§2.10) виртуальной машиной Java, и это исключение не обрабатывается внутри метода. Выполнение инструкции athrow (§athrow) также вызывает явное исключение, и если исключение не перехватывается текущим методом, то происходит прерывание вызова метода. Вызов метода, который завершается с прерыванием, никогда не возвращает значение вызывающему методу.
Виртуальная машина Java не накладывает никаких конкретных ограничений на внутреннюю структуру объектов.
В некоторых реализациях виртуальной машины Java от Oracle ссылка на экземпляр класса является указателем на обработчик, который сам по себе является парой указателей: один на таблицу, содержащую методы объекта, и указатель на Class объект, представляющий тип объекта, а другой - на выделенную из кучи память для данных объекта.
Виртуальная машина Java включает подмножество арифметики с плавающей точкой, указанное в Стандарте IEEE для двоичной арифметики с плавающей точкой (ANSI/IEEE Std. 754-1985, Нью-Йорк).
Ключевые различия между арифметикой с плавающей точкой, поддерживаемой виртуальной машиной Java, и стандартом IEEE 754:
-
Операции с плавающей точкой в виртуальной машине Java не генерируют исключения, не перехватывают и не сигнализируют о исключительных ситуациях IEEE 754 (неправильная операция, деление на ноль, переполнение, недополнение или неточность). Виртуальная машина Java не имеет значения NaN, сигнализирующего о неточности.
-
Виртуальная машина Java не поддерживает сигнализирующие сравнения с плавающей точкой IEEE 754.
-
Округления в виртуальной машине Java всегда используют режим округления IEEE 754 до ближайшего значения. Неточные результаты округляются до ближайшего представимого значения, а при равенстве выбирается значение с нулевым наименее значимым битом. Это – стандартный режим IEEE 754. Но инструкции виртуальной машины Java, которые преобразуют значения типов с плавающей точкой в значения целочисленных типов, округляют в сторону нуля. Виртуальная машина Java не предоставляет средства изменения режима округления с плавающей точкой.
-
Виртуальная машина Java не поддерживает ни расширенный формат IEEE 754 с одинарной, ни с двойной точностью, за исключением случаев, когда значения двойной точности и значения с двойной расширенной экспонентой могут поддерживать формат с одинарной точностью. Множества значений с расширенной экспонентой с плавающей точкой одинарной и двойной точности, которые могут быть необязательно поддерживаемы, не соответствуют значениям расширенных форматов IEEE 754: расширенные форматы IEEE 754 требуют расширенной точности и расширенного диапазона экспоненты.
Каждый метод имеет режим с плавающей точкой, который является либо FP-строгим, либо не FP-строгим. Режим с плавающей точкой метода определяется значением флага ACC_STRICT элемента access_flags структуры method_info (§4.6), определяющего метод. Метод, для которого установлен этот флаг, является FP-строгим; в противном случае, метод не FP-строгий.
Обратите внимание, что эта связь флага ACC_STRICT подразумевает, что методы в классах, скомпилированных компилятором JDK версии 1.1 или более ранней, фактически не являются FP-строгими.
Мы будем говорить, что стек операндов имеет данный режим с плавающей точкой, когда метод, вызов которого создал фрейм, содержащий стек операндов, имеет этот режим с плавающей точкой. Аналогично, мы будем говорить, что инструкция виртуальной машины Java имеет данный режим с плавающей точкой, когда метод, содержащий эту инструкцию, имеет этот режим.
Если поддерживается множество значений с плавающей точкой с расширенной экспонентой (§2.3.2), значения типа float в стеке операндов, который не является FP-строгим, могут охватывать это множество значений, за исключением случаев, запрещённых преобразованием множеств значений (§2.8.3). Если поддерживается множество значений с двойной расширенной экспонентой (§2.3.2), значения типа double в стеке операндов, который не является FP-строгим, могут охватывать это множество значений, за исключением случаев, запрещённых преобразованием множеств значений.
Во всех других контекстах, будь то в стеке операндов или где-либо ещё, и независимо от режима с плавающей точкой, значения с плавающей точкой типов float и double могут охватывать только множество значений типа float и double соответственно. В частности, поля классов и экземпляров, элементы массивов, локальные переменные и параметры методов могут содержать только значения, взятые из стандартных множеств значений.
Реализация виртуальной машины Java, поддерживающая расширенное множество значений с плавающей точкой, может или должна, в определённых обстоятельствах, отображать значение связанного типа с плавающей точкой между расширенным и стандартным множествами значений. Такое преобразование множества значений не является преобразованием типа, а отображением между множествами значений, связанных с одним типом.
В указанных ситуациях преобразования множества значений, реализация может выполнить одну из следующих операций над значением:
-
Если значение типа
floatи не является элементом множества значений float, оно отображается на ближайший элемент множества значений float. -
Если значение типа
doubleи не является элементом множества значений double, оно отображается на ближайший элемент множества значений double.
Кроме того, в указанных ситуациях преобразования множества значений требуются следующие операции:
-
Предположим, выполнение инструкции виртуальной машины Java, которая не является FP-строгой, вызывает помещение значения типа
floatв стек операндов, который является FP-строгим, передачу в качестве параметра или сохранение в локальную переменную, поле или элемент массива. Если значение не является элементом множества значений float, оно отображается на ближайший элемент множества значений float. -
Предположим, выполнение инструкции виртуальной машины Java, которая не является FP-строгой, вызывает помещение значения типа
doubleв стек операндов, который является FP-строгим, передачу в качестве параметра или сохранение в локальную переменную, поле или элемент массива. Если значение не является элементом множества значений double, оно отображается на ближайший элемент множества значений double.
Такие требуемые преобразования множеств значений могут произойти в результате передачи параметра типа с плавающей точкой во время вызова метода, включая вызов метода native; возвращения значения типа с плавающей точкой из метода, который не является FP-строгим, в метод, который является FP-строгим; или сохранения значения типа с плавающей точкой в локальную переменную, поле или массив в методе, который не является FP-строгим.
Не все значения из множества значений с расширенной экспонентой могут быть точно отображены в соответствующее значение в стандартном множестве значений. Если значение, которое отображается, слишком велико для точного представления (его экспонента больше, чем разрешено стандартным множеством значений), оно преобразуется в бесконечность (положительную или отрицательную) соответствующего типа. Если значение, которое отображается, слишком мало для точного представления (его экспонента меньше, чем разрешено стандартным множеством значений), оно округляется до ближайшего значения денормализованного значения или нуля того же знака.
Преобразование множеств значений сохраняет бесконечности и NaN и не может изменить знак преобразуемого значения. Преобразование множеств значений не влияет на значение, которое не является типом с плавающей точкой.
На уровне виртуальной машины Java, каждый конструктор, написанный на языке программирования Java (JLS §8.8), представлен как метод инициализации экземпляра, который имеет специальное имя <init>. Это имя предоставляется компилятором. Поскольку имя <init> не является допустимым идентификатором, его нельзя использовать напрямую в программе, написанной на языке программирования Java. Методы инициализации экземпляра могут вызываться только внутри виртуальной машины Java инструкцией invokespecial (§invokespecial), и они могут вызываться только для неинициализированных экземпляров класса. Метод инициализации экземпляра унаследует права доступа (JLS §6.6) от конструктора, из которого он был получен.
Класс или интерфейс имеют не более одного метода инициализации класса или интерфейса и инициализируется (§5.5) путем вызова этого метода. Метод инициализации класса или интерфейса имеет специальное имя <clinit>, не принимает аргументов и имеет тип void (§4.3.3).
Другие методы, имеющие имя <clinit> в файле class, не имеют никакого значения. Они не являются методами инициализации класса или интерфейса. Они не могут быть вызваны никакой инструкцией виртуальной машины Java, и виртуальная машина Java их никогда не вызывает.
В файле class, версия которого 51.0 или выше, метод должен дополнительно иметь свой флаг ACC_STATIC (§4.6), чтобы быть методом инициализации класса или интерфейса.
Это требование было введено в Java SE 7. В файле класса, версия которого 50.0 или ниже, метод с именем <clinit>, возвращающий void и не принимающий аргументов, считается методом инициализации класса или интерфейса независимо от значения флага ACC_STATIC.
Имя <clinit> предоставляется компилятором. Поскольку имя <clinit> не является допустимым идентификатором, его нельзя напрямую использовать в программе, написанной на языке программирования Java. Методы инициализации класса и интерфейса вызываются неявно виртуальной машиной Java; они никогда не вызываются напрямую из любой инструкции виртуальной машины Java, а вызываются только косвенно в рамках процесса инициализации класса.
Метод является полиморфным по сигнатуре, если все перечисленное ниже верно:
-
Он объявлен в классе
java.lang.invoke.MethodHandle. -
Он имеет один формальный параметр типа
Object[]. -
Он имеет тип возвращаемого значения
Object. -
У него установлены флаги
ACC_VARARGSиACC_NATIVE.
В Java SE 8 единственными полиморфными по сигнатуре методами являются методы invoke и invokeExact класса java.lang.invoke.MethodHandle.
Виртуальная машина Java предоставляет специальное обращение к полиморфным по сигнатуре методам в инструкции invokevirtual (§invokevirtual) для вызова обработчика метода. Обработчик метода — это строго типизированная, непосредственно исполняемая ссылка на базовый метод, конструктор, поле или аналогичную низкоуровневую операцию (§5.4.3.5), с необязательными преобразованиями аргументов или значений возврата. Эти преобразования довольно общие и включают такие шаблоны, как преобразование, вставка, удаление и подстановка. Дополнительную информацию можно найти в пакете java.lang.invoke API платформы Java SE.
Исключение в Java Virtual Machine представлено экземпляром класса Throwable или одного из его подклассов. Бросок исключения приводит к немедленному нелокальному переносу управления из точки, где было сгенерировано исключение.
Большинство исключений возникают синхронно в результате действия потока, в котором они происходят. В отличие от этого, асинхронное исключение потенциально может произойти в любой момент выполнения программы. Java Virtual Machine бросает исключение по одной из трёх причин:
-
Была выполнена инструкция athrow (§athrow).
-
Синхронно было обнаружено аномальное состояние выполнения Java Virtual Machine. Такие исключения не выбрасываются в произвольной точке программы, а только синхронно после выполнения инструкции, которая либо:
-
Указывает исключение как возможный результат, например:
-
Когда инструкция воплощает операцию, которая нарушает семантику языка программирования Java, например, индексирование за пределами границ массива.
-
Когда происходит ошибка при загрузке или связывании части программы.
-
-
Приводит к превышению какого-либо ограничения на ресурс, например, когда используется слишком много памяти.
-
-
Произошло асинхронное исключение, потому что:
-
Вызван метод
stopклассаThreadилиThreadGroup, или -
Возникла внутренняя ошибка в реализации Java Virtual Machine.
Методы
stopмогут вызываться одним потоком для воздействия на другой поток или на все потоки в указанной группе потоков. Они являются асинхронными, потому что могут произойти в любой момент выполнения другого потока или потоков. Внутренняя ошибка считается асинхронной (§6.3). -
Java Virtual Machine может разрешить небольшое, но ограниченное количество выполнения перед тем, как будет сгенерировано асинхронное исключение. Эта задержка разрешена для оптимизированного кода, чтобы обнаружить и бросить эти исключения в точках, где их удобно обработать, соблюдая при этом семантику языка программирования Java.
Простая реализация могла бы периодически проверять наличие асинхронных исключений в точке каждой инструкции перехода управления. Поскольку программа имеет конечный размер, это обеспечивает ограничение общей задержки при обнаружении асинхронного исключения. Поскольку асинхронное исключение не произойдёт между переходами управления, генератор кода обладает некоторой гибкостью для переупорядочивания вычислений между переходами управления для повышения производительности. Рекомендуется к прочтению статья Polling Efficiently on Stock Hardware Марка Фели, Proc. 1993 Conference on Functional Programming and Computer Architecture, Копенгаген, Дания, стр. 179–187.
Исключения, сгенерированные Java Virtual Machine, являются точными: когда происходит перенос управления, все эффекты инструкций, выполненных до точки, из которой выбрасывается исключение, должны казаться выполненными. Никакие инструкции, которые выполняются после точки, из которой выбрасывается исключение, не могут казаться выполненными. Если оптимизированный код упреждающе выполнил некоторые инструкции, следующие за точкой, в которой возникает исключение, такой код должен быть готов скрыть это упреждающее выполнение от видимого пользователю состояния программы.
Каждый метод в Java Virtual Machine может быть связан с нулём или более обработчиками исключений. Обработчик исключений указывает диапазон смещений в коде Java Virtual Machine, реализующем метод, для которого активен обработчик исключений, описывает тип исключения, которое может обработать обработчик исключений, и указывает расположение кода, который должен обрабатывать это исключение. Исключение соответствует обработчику исключений, если смещение инструкции, вызвавшей исключение, находится в диапазоне смещений обработчика исключений и тип исключения — тот же класс или подкласс, что и класс исключения, обрабатываемый обработчиком исключений. Когда возникает исключение, Java Virtual Machine ищет соответствующий обработчик исключений в текущем методе. Если такой обработчик найден, система переходит к коду обработки исключений, указанному соответствующим обработчиком.
Если такой обработчик исключений не найден в текущем методе, вызов текущего метода завершается внезапно (§2.6.5). При внезапном завершении стек операндов и локальные переменные текущего вызова метода удаляются, а его фрейм удаляется, восстанавливая фрейм вызывающего метода. Затем исключение повторно выбрасывается в контексте фрейма вызывающего метода и так далее, продолжая вверх по цепочке вызова метода. Если подходящий обработчик исключений не найден до тех пор, пока не будет достигнут верхний уровень цепочки вызова метода, выполнение потока, в котором было сгенерировано исключение, завершается.
Порядок поиска обработчиков исключений метода важен. В файле class обработчики исключений для каждого метода хранятся в таблице (§4.7.3). Во время выполнения, когда возникает исключение, Java Virtual Machine ищет обработчики исключений текущего метода в том порядке, в котором они появляются в соответствующей таблице обработчиков исключений в файле class, начиная с начала этой таблицы.
Обратите внимание, что Java Virtual Machine не навязывает вложенность или любой порядок записей таблицы исключений метода. Семантика обработки исключений языка программирования Java реализуется только через сотрудничество с компилятором (§3.12). Когда файлы class генерируются другими средствами, определённая процедура поиска гарантирует, что все реализации Java Virtual Machine будут работать согласованно.
Инструкция виртуальной машины Java состоит из одного байта кода операции, определяющего выполняемое действие, за которым следуют нуль или более операндов, предоставляющих аргументы или данные, используемые операцией. Многие инструкции не имеют операндов и состоят только из кода операции.
Игнорируя исключения, внутренний цикл интерпретатора виртуальной машины Java эффективно
do {
atomically calculate pc and fetch opcode at pc;
if (operands) fetch operands;
execute the action for the opcode;
} while (there is more to do);
Количество и размер операндов определяются кодом операции. Если размер операнда больше одного байта, то он хранится в порядке big-endian — старший байт первым. Например, 16-битный беззнаковый индекс в локальных переменных хранится как два беззнаковых байта, byte1 и byte2, так что его значение равно (byte1 << 8) | byte2.
Поток инструкций байткода выровнен только по одному байту. Два исключения составляют инструкции lookupswitch и tableswitch (§lookupswitch, §tableswitch), которые дополняются для принудительного внутреннего выравнивания некоторых своих операндов по границам в 4 байта.
Решение ограничить код операции виртуальной машины Java одним байтом и отказаться от выравнивания данных в скомпилированном коде отражает сознательную предвзятость в пользу компактности, возможно, ценой некоторой производительности в простых реализациях. Один байт кода операции также ограничивает размер набора инструкций. Не предполагая выравнивания данных, означает, что непосредственные данные, превышающие один байт, должны быть построены из байтов во время выполнения на многих машинах.
Большинство инструкций в наборе инструкций виртуальной машины Java кодируют информацию о типах выполняемых операций. Например, инструкция iload (§iload) загружает содержимое локальной переменной, которая должна быть int, на стек операндов. Инструкция fload (§fload) делает то же самое со значением float. Эти две инструкции могут иметь одинаковые реализации, но имеют разные коды операций.
Для большинства типизированных инструкций тип инструкции явно представлен в мнемоническом обозначении кода операции буквой: i для int операции, l для long, s для short, b для byte, c для char, f для float, d для double и a для reference. Некоторые инструкции, для которых тип однозначен, не имеют буквы типа в своём мнемоническом обозначении. Например, arraylength всегда работает с объектом, который является массивом. Некоторые инструкции, такие как goto, безусловный переход управления, не работают с типизированными операндами.
Учитывая размер кода операции виртуальной машины Java в один байт, кодирование типов в коды операций создаёт давление на проектирование её набора инструкций. Если каждая типизированная инструкция поддерживала все типы данных времени выполнения виртуальной машины Java, то инструкций было бы больше, чем можно представить в одном байте. Вместо этого, набор инструкций виртуальной машины Java предоставляет уменьшенный уровень поддержки типов для определённых операций. Другими словами, набор инструкций преднамеренно не ортогонален. Для преобразования между неподдерживаемыми и поддерживаемыми типами данных при необходимости можно использовать отдельные инструкции.
Таблица 2.11.1-A обобщает поддержку типов в наборе инструкций виртуальной машины Java. Конкретная инструкция с информацией о типе создаётся путём замены T в шаблоне инструкции в столбце кода операции буквой в столбце типа. Если столбец типа для некоторого шаблона инструкции и типа пуст, то нет инструкции, поддерживающей данный тип операции. Например, есть инструкция загрузки для типа int, iload, но нет инструкции загрузки для типа byte.
Обратите внимание, что большинство инструкций в таблице 2.11.1-A не имеют форм для целочисленных типов byte, char и short. Ни у одной из них нет форм для типа boolean. Компилятор кодирует загрузки литеральных значений типов byte и short с использованием инструкций виртуальной машины Java, которые расширяют эти значения со знаком до значений типа int во время компиляции или выполнения. Загрузки литеральных значений типов boolean и char кодируются с помощью инструкций, которые беззнаково расширяют литерал до значения типа int во время компиляции или выполнения. Аналогично, загрузки из массивов значений типов boolean, byte, short и char кодируются с использованием инструкций виртуальной машины Java, которые расширяют значения со знаком или без знака до значений типа int. Таким образом, большинство операций над значениями фактических типов boolean, byte, char и short правильно выполняются инструкциями, работающими со значениями вычислительного типа int.
Таблица 2.11.1-A. Поддержка типов в наборе команд виртуальной машины Java
| opcode | byte | short | int | long | float | double | char | reference |
|---|---|---|---|---|---|---|---|---|
| Tipush | bipush | sipush | ||||||
| Tconst | iconst | lconst | fconst | dconst | aconst | |||
| Tload | iload | lload | fload | dload | aload | |||
| Tstore | istore | lstore | fstore | dstore | astore | |||
| Tinc | iinc | |||||||
| Taload | baload | saload | iaload | laload | faload | daload | caload | aaload |
| Tastore | bastore | sastore | iastore | lastore | fastore | dastore | castore | aastore |
| Tadd | iadd | ladd | fadd | dadd | ||||
| Tsub | isub | lsub | fsub | dsub | ||||
| Tmul | imul | lmul | fmul | dmul | ||||
| Tdiv | idiv | ldiv | fdiv | ddiv | ||||
| Trem | irem | lrem | frem | drem | ||||
| Tneg | ineg | lneg | fneg | dneg | ||||
| Tshl | ishl | lshl | ||||||
| Tshr | ishr | lshr | ||||||
| Tushr | iushr | lushr | ||||||
| Tand | iand | land | ||||||
| Tor | ior | lor | ||||||
| Txor | ixor | lxor | ||||||
| i2T | i2b | i2s | i2l | i2f | i2d | |||
| l2T | l2i | l2f | l2d | |||||
| f2T | f2i | f2l | f2d | |||||
| d2T | d2i | d2l | d2f | |||||
| Tcmp | lcmp | |||||||
| Tcmpl | fcmpl | dcmpl | ||||||
| Tcmpg | fcmpg | dcmpg | ||||||
| if_TcmpOP | if_icmpOP | if_acmpOP | ||||||
| Treturn | ireturn | lreturn | freturn | dreturn | areturn |
Сопоставление фактических типов виртуальной машины Java и вычислительных типов виртуальной машины Java суммируется в таблице 2.11.1-B.
Некоторые инструкции виртуальной машины Java, такие как pop и swap, работают со стеком операндов без учета типа; однако такие инструкции ограничены использованием только значений определенных категорий вычислительных типов, также указанных в таблице 2.11.1-B.
Таблица 2.11.1-B. Фактические и вычислительные типы в виртуальной машине Java
| Фактический тип | Вычислительный тип | Категория |
|---|---|---|
boolean | int | 1 |
byte | int | 1 |
char | int | 1 |
short | int | 1 |
int | int | 1 |
float | float | 1 |
reference | reference | 1 |
returnAddress | returnAddress | 1 |
long | long | 2 |
double | double | 2 |
Инструкции загрузки и сохранения переносят значения между локальными переменными (§2.6.1) и стеком операндов (§2.6.2) кадра виртуальной машины Java (§2.6):
-
Загрузка локальной переменной в стек операндов: iload, iload_<n>, lload, lload_<n>, fload, fload_<n>, dload, dload_<n>, aload, aload_<n>.
-
Сохранение значения из стека операндов в локальную переменную: istore, istore_<n>, lstore, lstore_<n>, fstore, fstore_<n>, dstore, dstore_<n>, astore, astore_<n>.
-
Загрузка константы в стек операндов: bipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_m1, iconst_<i>, lconst_<l>, fconst_<f>, dconst_<d>.
-
Доступ к большему количеству локальных переменных с использованием более широкого индекса или к более большому непосредственному операнду: wide.
Инструкции, которые обращаются к полям объектов и элементам массивов (§2.11.5), также передают данные в стек операндов и из него.
Мемонические обозначения инструкций, показанные выше с заключительными буквами в угловых скобках (например, iload_<n>), обозначают семейства инструкций (с членами iload_0, iload_1, iload_2 и iload_3 в случае iload_<n>). Такие семейства инструкций являются специализациями дополнительной универсальной инструкции (iload), которая принимает один операнд. Для специализированных инструкций операнд подразумевается и не нужно хранить или извлекать. Семантика в остальном такая же (iload_0 означает то же самое, что и iload с операндом 0). Буква между угловыми скобками указывает тип неявного операнда для этого семейства инструкций: для <n> - неотрицательное целое число; для <i> - int; для <l> - long; для <f> - float; и для <d> - double. Формы для типа int используются во многих случаях для выполнения операций над значениями типа byte, char и short (§2.11.1).
Эта нотация для семейств инструкций используется на протяжении всего данного спецификации.
Арифметические инструкции вычисляют результат, который, как правило, является функцией двух значений в стеке операндов, помещая результат обратно в стек операндов. Существует два основных типа арифметических инструкций: те, которые работают со значениями целых чисел, и те, которые работают с плавающей точкой. В каждом из этих типов арифметические инструкции специализированы для числовых типов виртуальной машины Java. Нет прямой поддержки арифметики целых чисел над значениями типов byte, short и char (§2.11.1) или над значениями типа boolean; эти операции обрабатываются инструкциями, работающими над типом int. Инструкции целых чисел и чисел с плавающей точкой также различаются по своему поведению при переполнении и делении на ноль. Арифметические инструкции следующие:
-
Сложение: iadd, ladd, fadd, dadd.
-
Вычитание: isub, lsub, fsub, dsub.
-
Умножение: imul, lmul, fmul, dmul.
-
Деление: idiv, ldiv, fdiv, ddiv.
-
Остаток: irem, lrem, frem, drem.
-
Отрицание: ineg, lneg, fneg, dneg.
-
Сдвиг: ishl, ishr, iushr, lshl, lshr, lushr.
-
Побитовое ИЛИ: ior, lor.
-
Побитовое И: iand, land.
-
Исключающее ИЛИ: ixor, lxor.
-
Инкремент локальной переменной: iinc.
-
Сравнение: dcmpg, dcmpl, fcmpg, fcmpl, lcmp.
Семантика операторов языка программирования Java над значениями целых чисел и чисел с плавающей точкой (JLS §4.2.2, JLS §4.2.4) непосредственно поддерживаются семантикой набора инструкций виртуальной машины Java.
Виртуальная машина Java не указывает переполнение при операциях над целочисленными типами данных. Единственные целочисленные операции, которые могут вызвать исключение, — это инструкции целочисленного деления (idiv и ldiv) и инструкции целочисленного остатка (irem и lrem), которые выбрасывают исключение ArithmeticException, если делитель равен нулю.
Операции виртуальной машины Java над числами с плавающей точкой ведут себя так, как указано в IEEE 754. В частности, виртуальная машина Java требует полной поддержки денормализованных чисел с плавающей точкой IEEE 754 и постепенного уменьшения, что облегчает доказательство желаемых свойств конкретных численных алгоритмов.
Виртуальная машина Java требует, чтобы арифметические операции с плавающей точкой вели себя так, как если бы каждый оператор с плавающей точкой округлял свой результат с плавающей точкой до точности результата. Неточные результаты должны быть округлены до ближайшего представимого значения, близкого к бесконечно точному результату; если два ближайших представимых значения находятся на одинаковом расстоянии, выбирается то, у которого наименее значимый бит равен нулю. Это стандартный режим округления IEEE 754, известный как режим округления до ближайшего значения.
Виртуальная машина Java использует режим округления к нулю IEEE 754 при преобразовании значения с плавающей точкой в целое число. Это приводит к усечению числа; любые биты значащих разрядов, которые представляют дробную часть значения операнда, отбрасываются. Режим округления к нулю выбирает в качестве результата значение типа, самое близкое к, но не больше по величине, чем бесконечно точный результат.
Операторы виртуальной машины Java с плавающей точкой не генерируют исключения во время выполнения (не следует путать с исключениями с плавающей точкой IEEE 754). Операция, которая переполняет, создает бесконечность со знаком, операция, которая уменьшается, создает денормализованное значение или нуль со знаком, и операция, у которой нет математически определенного результата, создает NaN. Все числовые операции с NaN в качестве операнда дают NaN в качестве результата.
Сравнения значений типа long (lcmp) выполняют сравнение со знаком. Сравнения значений с плавающей точкой (dcmpg, dcmpl, fcmpg, fcmpl) выполняются с использованием безысключительных сравнений IEEE 754.
Инструкции преобразования типов позволяют преобразовывать числовые типы виртуальной машины Java. Они могут использоваться для реализации явных преобразований в пользовательском коде или для смягчения недостатка ортогональности в наборе инструкций виртуальной машины Java.
Виртуальная машина Java напрямую поддерживает следующие расширяющие числовые преобразования:
-
intвlong,floatилиdouble -
longвfloatилиdouble -
floatвdouble
Инструкции расширяющего числового преобразования — это i2l, i2f, i2d, l2f, l2d и f2d. Мемоники для этих опкодов очевидны, учитывая соглашения об именовании для типизированных инструкций и использование двойного смысла 2, означающего «в». Например, инструкция i2d преобразует значение типа int в значение типа double.
Большинство расширяющих числовых преобразований не теряют информацию о величине числового значения. Действительно, преобразования, расширяющие от int до long и от int до double, не теряют никакой информации; числовое значение сохраняется точно. Преобразования, расширяющие от float до double и являющиеся строго плавающей точкой (§2.8.2), также сохраняют числовое значение точно; только такие преобразования, которые не являются строго плавающей точкой, могут потерять информацию о величине преобразованного значения.
Преобразования от int к float или от long к float или от long к double могут потерять точность, то есть могут потерять некоторые младшие биты значения; полученное значение с плавающей точкой — это правильно округленное значение целочисленного значения, используя режим округления до ближайшего IEEE 754.
Несмотря на то, что может произойти потеря точности, расширяющие числовые преобразования никогда не вызывают исключения во время выполнения в виртуальной машине Java (не следует путать с исключением плавающей точки IEEE 754).
Расширяющее числовое преобразование int в long просто расширяет знак представления int значения с дополнительным кодом до более широкого формата. Расширяющее числовое преобразование char в целочисленный тип нулевое расширение представления char значения до более широкого формата.
Обратите внимание, что расширяющие числовые преобразования целочисленных типов byte, char и short в тип int не существуют. Как указано в §2.11.1, значения типов byte, char и short неявно расширяются до типа int, делая эти преобразования неявными.
Виртуальная машина Java также напрямую поддерживает следующие сужающие числовые преобразования:
-
intвbyte,shortилиchar -
longвint -
floatвintилиlong -
doubleвint,longилиfloat
Инструкции сужающего числового преобразования — это i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l и d2f. Сужающее числовое преобразование может привести к значению с другим знаком, другой величиной порядка или обоим; при этом может произойти потеря точности.
Сужающее числовое преобразование int или long в целочисленный тип T просто отбрасывает все, кроме n младших разрядов, где n — количество разрядов, используемых для представления типа T. Это может привести к тому, что результирующее значение не будет иметь того же знака, что и входное значение.
При сужающем числовом преобразовании значения с плавающей точкой в целочисленный тип T, где T — это либо int, либо long, значение с плавающей точкой преобразуется следующим образом:
-
Если значение с плавающей точкой — NaN, результат преобразования —
intилиlong0. -
В противном случае, если значение с плавающей точкой не является бесконечностью, значение с плавающей точкой округляется до целочисленного значения V с помощью режима округления IEEE 754 к нулю. Есть два случая:
-
Если T —
long, и это целочисленное значение может быть представлено какlong, то результат —longзначение V. -
Если T — типа
int, и это целочисленное значение может быть представлено какint, то результат —intзначение V.
-
-
В противном случае:
-
Или значение должно быть слишком малым (отрицательное значение большой величины или отрицательная бесконечность), и результатом является наименьшее представимое значение типа
intилиlong. -
Или значение должно быть слишком большим (положительное значение большой величины или положительная бесконечность), и результатом является наибольшее представимое значение типа
intилиlong.
-
Сужающее числовое преобразование от double к float выполняется в соответствии с IEEE 754. Результат правильно округляется с помощью режима округления IEEE 754 до ближайшего значения. Значение, слишком малое для представления как float, преобразуется в положительный или отрицательный ноль типа float; значение, слишком большое для представления как float, преобразуется в положительную или отрицательную бесконечность. double NaN всегда преобразуется в float NaN.
Несмотря на то, что могут произойти переполнение, недополнение или потеря точности, сужающие преобразования между числовыми типами никогда не вызывают исключения во время выполнения в виртуальной машине Java (не следует путать с исключением плавающей точки IEEE 754).
Хотя экземпляры классов и массивы являются объектами, виртуальная машина Java создает и обрабатывает экземпляры классов и массивы с использованием различных наборов инструкций:
-
Создание нового экземпляра класса: new.
-
Создание нового массива: newarray, anewarray, multianewarray.
-
Доступ к полям классов (
staticполя, известные как переменные класса) и полям экземпляров классов (не-staticполя, известные как переменные экземпляров): getstatic, putstatic, getfield, putfield. -
Загрузка компонента массива на стек операндов: baload, caload, saload, iaload, laload, faload, daload, aaload.
-
Сохранение значения из стека операндов как компонента массива: bastore, castore, sastore, iastore, lastore, fastore, dastore, aastore.
-
Получение длины массива: arraylength.
-
Проверка свойств экземпляров классов или массивов: instanceof, checkcast.
Для непосредственного управления стеком операндов предоставляется ряд инструкций: pop, pop2, dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2, swap.
Инструкции передачи управления условно или безусловно заставляют виртуальную машину Java продолжить выполнение с инструкцией, отличной от следующей за инструкцией передачи управления. Это:
-
Условный переход: ifeq, ifne, iflt, ifle, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_icmple, if_icmpgt if_icmpge, if_acmpeq, if_acmpne.
-
Составной условный переход: tableswitch, lookupswitch.
-
Безусловный переход: goto, goto_w, jsr, jsr_w, ret.
У виртуальной машины Java есть отдельные наборы инструкций, которые условно переходят по сравнению с данными типов int и reference. Она также имеет отдельные инструкции условного перехода, которые проверяют на null-ссылку, и поэтому нет необходимости указывать конкретное значение для null (§2.4).
Условные переходы по сравнениям между данными типов boolean, byte, char и short выполняются с помощью инструкций сравнения int (§2.11.1). Условный переход по сравнению между данными типов long, float или double инициируется с помощью инструкции, которая сравнивает данные и производит результат сравнения int (§2.11.3). Последующая инструкция сравнения int проверяет этот результат и осуществляет условный переход. Из-за акцента на сравнениях int, виртуальная машина Java предоставляет богатый набор инструкций условного перехода для типа int.
Все инструкции условного перехода int выполняют знаковое сравнение.
Следующие пять инструкций вызывают методы:
-
invokevirtual вызывает метод экземпляра объекта, распределяя по (виртуальному) типу объекта. Это обычный метод диспетчеризации в языке программирования Java.
-
invokeinterface вызывает метод интерфейса, просматривая методы, реализованные конкретным объектом во время выполнения, чтобы найти соответствующий метод.
-
invokespecial вызывает метод экземпляра, требующий специальной обработки, будь то метод инициализации экземпляра (§2.9), метод
privateили метод суперкласса. -
invokestatic вызывает метод класса (
static) в указанном классе. -
invokedynamic вызывает метод, который является целевым объектом, связанным с инструкцией вызова invokedynamic. Объект сайта вызова был связан с определённым лексическим случаем инструкции invokedynamic виртуальной машиной Java в результате запуска метода загрузки перед первым выполнением инструкции. Поэтому каждая встреча инструкции invokedynamic имеет уникальное состояние связи, в отличие от других инструкций, которые вызывают методы.
Инструкции возврата метода, которые различаются по типу возвращаемого значения, это ireturn (используется для возврата значений типа boolean, byte, char, short или int), lreturn, freturn, dreturn и areturn. Кроме того, инструкция return используется для возврата из методов, объявленных как void, методов инициализации экземпляров и методов инициализации классов или интерфейсов.
Исключение выбрасывается программно с помощью инструкции athrow. Исключение также может быть вызвано различными инструкциями виртуальной машины Java, если они обнаруживают аномальное состояние.
Виртуальная машина Java поддерживает синхронизацию как методов, так и последовательностей инструкций в методе с помощью одного синхронизирующего конструктора: монитор.
Синхронизация на уровне метода выполняется неявно, как часть вызова и возврата метода (§2.11.8). Метод synchronized отличается в структуре method_info пула постоянных времени выполнения (§4.6) флагом ACC_SYNCHRONIZED, который проверяется инструкциями вызова метода. При вызове метода, для которого установлен ACC_SYNCHRONIZED, выполняющая нить входит в монитор, вызывает сам метод и выходит из монитора, независимо от того, завершился ли вызов метода нормально или прерывисто. В то время, когда выполняющая нить владеет монитором, ни одна другая нить не может войти в него. Если исключение возникает во время вызова метода synchronized и метод synchronized не обрабатывает это исключение, монитор метода автоматически выходит из него, прежде чем исключение будет повторно выброшено из метода synchronized.
Синхронизация последовательностей инструкций обычно используется для кодирования блока synchronized языка программирования Java. Виртуальная машина Java предоставляет инструкции monitorenter и monitorexit для поддержки таких языковых конструкций. Правильная реализация блоков synchronized требует сотрудничества компилятора, ориентированного на виртуальную машину Java (§3.14).
Структурированное блокирование возникает, когда во время вызова метода каждый выход из данного монитора соответствует предшествующему входу в этот монитор. Поскольку нет гарантии, что весь код, представленный виртуальной машине Java, будет выполнять структурированное блокирование, реализации виртуальной машины Java разрешено, но не требуется, применять следующие два правила, гарантирующие структурированное блокирование. Пусть T — это нить, а M — монитор. Тогда:
-
Количество входов в монитор, выполненных T на M во время вызова метода, должно быть равно количеству выходов из монитора, выполненных T на M во время вызова метода, независимо от того, завершился ли вызов метода нормально или прерывисто.
-
Ни в какой момент времени во время вызова метода количество выходов из монитора, выполненных T на M с момента вызова метода, не может превышать количество входов в монитор, выполненных T на M с момента вызова метода.
Обратите внимание, что автоматический вход и выход из монитора, выполняемый виртуальной машиной Java при вызове метода synchronized, считаются происходящими во время вызова вызывающего метода.
Виртуальная машина Java должна обеспечивать достаточную поддержку для реализации библиотек классов платформы Java SE. Некоторые классы в этих библиотеках не могут быть реализованы без сотрудничества с виртуальной машиной Java.
К классам, которые могут потребовать специальной поддержки от виртуальной машины Java, относятся те, которые поддерживают:
-
Рефлексию, такие как классы в пакете
java.lang.reflectи классClass. -
Загрузку и создание класса или интерфейса. Наиболее очевидным примером является класс
ClassLoader. -
Связывание и инициализацию класса или интерфейса. Приведенные выше примеры классов также попадают в эту категорию.
-
Безопасность, такие как классы в пакете
java.securityи другие классы, такие какSecurityManager. -
Многопоточность, такой как класс
Thread. -
Слабые ссылки, такие как классы в пакете
java.lang.ref.
Вышеприведенный список призван быть иллюстративным, а не исчерпывающим. Полный перечень этих классов или предоставляемых ими функций выходит за рамки этого спецификационного документа. Подробности см. в спецификациях библиотек классов платформы Java SE.
До настоящего момента эта спецификация очерчивала публичный вид виртуальной машины Java: формат файла class и набор инструкций. Эти компоненты имеют важное значение для независимости от аппаратного обеспечения, операционной системы и реализации виртуальной машины Java. Реализатор может предпочесть рассматривать их как средство безопасной передачи фрагментов программ между хостами, каждый из которых реализует платформу Java SE, а не как точное руководство для следования.
Важно понимать, где проходит граница между публичным дизайном и приватной реализацией. Реализация виртуальной машины Java должна уметь читать файлы class и точно реализовывать семантику кода виртуальной машины Java в них. Один из способов сделать это — принять этот документ как спецификацию и буквально реализовать эту спецификацию. Но также вполне осуществимо и желательно, чтобы реализатор мог модифицировать или оптимизировать реализацию в рамках ограничений этой спецификации. До тех пор, пока формат файла class может быть прочитан, а семантика его кода поддерживается, реализатор может реализовать эти семантики любым способом. Что «под капотом» — дело реализатора, пока поддерживается правильный внешний интерфейс.
Существуют некоторые исключения: отладчики, профилировщики и генераторы кода Just-In-Time могут потребовать доступа к элементам виртуальной машины Java, которые обычно считаются «под капотом». В соответствующих случаях Oracle сотрудничает с другими реализаторами виртуальных машин Java и поставщиками инструментов для разработки общих интерфейсов с виртуальной машиной Java для использования такими инструментами и для продвижения этих интерфейсов по всей отрасли.
Реализатор может использовать эту гибкость для настройки реализаций виртуальной машины Java на высокую производительность, низкое использование памяти или переносимость. Что имеет смысл в данной реализации, зависит от целей этой реализации. Диапазон вариантов реализации включает следующее:
-
Перевод кода виртуальной машины Java во время загрузки или во время выполнения в набор инструкций другой виртуальной машины.
-
Перевод кода виртуальной машины Java во время загрузки или во время выполнения в машинный код процессора хоста (иногда это называют генерацией кода just-in-time, или JIT).
Существование точно определенного виртуального машинного и объектного форматов файлов не должно существенно ограничивать творческий потенциал реализатора. Виртуальная машина Java разработана для поддержки многих различных реализаций, предоставляя новые и интересные решения при сохранении совместимости между реализациями.
© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.