Глава 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 в этой спецификации приводятся в соответствии с The Unicode Standard, Version 10.0.0, доступным по адресу http://www.unicode.org/.
Компилируемый код для выполнения виртуальной машиной Java представлен с помощью независимого от аппаратного обеспечения и операционной системы двоичного формата, обычно (но не обязательно) хранящегося в файле, известном как формат файла class. Формат файла class точно определяет представление класса или интерфейса, включая такие детали, как порядок байтов, которые могут быть приняты как должное в формате платформо-специфического объектного файла.
Глава 4, "Формат файла class", подробно рассматривает формат файла class.
Как и язык программирования Java, виртуальная машина Java работает с двумя видами типов: примитивные типы и ссылочные типы. Соответственно, есть два вида значений, которые могут храниться в переменных, передаваться в качестве аргументов, возвращаться методами и обрабатываться: примитивные значения и ссылочные значения.
Виртуальная машина Java ожидает, что практически вся проверка типов выполняется до времени выполнения, как правило, компилятором, и не должна выполняться самой виртуальной машиной Java. Значения примитивных типов не должны быть помечены или иным образом проверяемы, чтобы определить их тип во время выполнения или отличить их от значений ссылочных типов. Вместо этого набор инструкций виртуальной машины Java различает типы своих операндов, используя инструкции, предназначенные для работы со значениями определенных типов. Например, iadd, ladd, fadd и dadd являются инструкциями виртуальной машины Java, которые складывают два числовых значения и производят числовые результаты, но каждая специализирована для своего типа операнда: int, long, float и double соответственно. Подробнее о поддержке типов в наборе инструкций виртуальной машины Java см. §2.11.1.
Виртуальная машина 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 для двоичной арифметики с плавающей точкой (ANSI/IEEE Std. 754-1985, Нью-Йорк).
Стандарт IEEE 754 включает не только положительные и отрицательные числа со знаком и величиной, но также положительные и отрицательные нули, положительные и отрицательные бесконечности и специальное значение Не число (в дальнейшем сокращенно «NaN»). Значение NaN используется для представления результата некоторых недопустимых операций, таких как деление нуля на ноль.
Каждая реализация виртуальной машины Java обязана поддерживать два стандартных набора значений с плавающей точкой, называемые множеством значений float и множеством значений double. Кроме того, реализация виртуальной машины Java может по своему усмотрению поддерживать одно или оба расширенных набора значений с плавающей точкой с расширенным порядком, называемые множеством значений 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-A.
Таблица 2.3.2-A. Параметры набора значений с плавающей точкой
| Параметр | float | float с расширенным порядком | double | double с расширенным порядком |
|---|---|---|---|---|
| N | 24 | 24 | 53 | 53 |
| K | 8 | ≥ 11 | 11 | ≥ 15 |
| Emax | +127 | ≥ +1023 | +1023 | ≥ +16383 |
| Emin | -126 | ≤ -1022 | -1022 | ≤ -16382 |
Если реализация поддерживает один или оба расширенных набора значений с плавающей точкой, то для каждого поддерживаемого расширенного набора значений с плавающей точкой существует определённая, зависящая от реализации, константа K, значение которой ограничено Таблицей 2.3.2-A; это значение K в свою очередь определяет значения для Emin и Emax.
Каждый из четырёх наборов значений включает не только конечные ненулевые значения, указанные выше, но также пять значений: положительный ноль, отрицательный ноль, положительная бесконечность, отрицательная бесконечность и NaN.
Обратите внимание, что ограничения в Таблице 2.3.2-A разработаны таким образом, что каждый элемент набора значений 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 всегда корректно использовать элемент набора значений float для представления значения типа float; однако в некоторых контекстах реализация может использовать элемент набора значений float с расширенным порядком вместо этого. Аналогично, всегда корректно использовать элемент набора значений double для представления значения типа double; однако в некоторых контекстах реализация может использовать элемент набора значений double с расширенным порядком вместо этого.
За исключением NaN, значения наборов значений с плавающей точкой упорядочены. При упорядочении от наименьшего к наибольшему они представляют собой отрицательную бесконечность, отрицательные конечные значения, положительный и отрицательный ноль, положительные конечные значения и положительную бесконечность.
Положительный ноль с плавающей точкой и отрицательный ноль с плавающей точкой сравниваются как равные, но существуют и другие операции, которые могут их различать; например, деление 1.0 на 0.0 даёт положительную бесконечность, но деление 1.0 на -0.0 даёт отрицательную бесконечность.
NaN не упорядочены, поэтому числовые сравнения и проверки на числовое равенство имеют значение false, если один или оба операнда равны NaN. В частности, проверка числового равенства значения самому себе имеет значение false тогда и только тогда, когда значение равно NaN. Проверка числового неравенства имеет значение true, если один из операндов равен NaN.
Тип returnAddress используется инструкциями виртуальной машины Java jsr, ret и jsr_w (§jsr, §ret, §jsr_w). Значения типа returnAddress — указатели на коды инструкций виртуальной машины Java. В отличие от числовых примитивных типов, тип 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 содержит адрес инструкции виртуальной машины Java, которая в данный момент выполняется. Если метод, который в данный момент выполняется потоком, native, то значение регистра виртуальной машины pc не определено. Регистр виртуальной машины pc достаточно широк, чтобы содержать returnAddress или указатель нативный на конкретной платформе.
Каждый поток виртуальной машины Java имеет свой собственный частный стек виртуальной машины Java, создаваемый одновременно с потоком. Стек виртуальной машины Java хранит фреймы (§2.6). Стек виртуальной машины Java аналогичен стеку традиционного языка программирования, такого как C: он хранит локальные переменные и частичные результаты, играет роль в вызове и возврате методов. Поскольку стек виртуальной машины Java никогда не манипулируется напрямую, кроме как для помещения и извлечения фреймов, фреймы могут быть выделены в куче. Память для стека виртуальной машины Java не обязательно должна быть непрерывной.
В первом издании спецификации виртуальной машины The Java® Virtual Machine Specification стек виртуальной машины 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.
Метод является методом инициализации экземпляра, если все из перечисленного ниже истинно:
-
Он определен в классе (а не в интерфейсе).
-
У него есть специальное имя
<init>. -
Он является
void(§4.3.3).
В классе любой метод, не являющийся void, с именем <init>, не является методом инициализации экземпляра. В интерфейсе любой метод с именем <init> не является методом инициализации экземпляра. Такие методы не могут быть вызваны никакой инструкцией Java Virtual Machine (§4.4.2, §4.9.2) и отклоняются проверкой формата (§4.6, §4.8).
Объявление и использование метода инициализации экземпляра ограничены Java Virtual Machine. Для объявления ограничены элемент access_flags и массив code (§4.6, §4.9.2). Для использования метод инициализации экземпляра может быть вызван только инструкцией invokespecial для неинициализированного экземпляра класса (§4.10.1.9).
Поскольку имя <init> не является допустимым идентификатором в языке программирования Java, его нельзя использовать непосредственно в программе, написанной на языке программирования Java.
Класс или интерфейс имеет не более одного метода инициализации класса или интерфейса и инициализируется Java Virtual Machine путем вызова этого метода (§5.5).
Метод является методом инициализации класса или интерфейса, если все перечисленные ниже условия истинны:
-
У него есть специальное имя
<clinit>. -
Он является
void(§4.3.3). -
В файле
class, версия которого 51.0 или выше, для метода установлен флагACC_STATICи он не принимает аргументов (§4.6).Требование к
ACC_STATICбыло введено в Java SE 7, а требование к отсутствию аргументов — в Java SE 9. В файле класса, версия которого 50.0 или ниже, метод с именем<clinit>, являющийсяvoid, считается методом инициализации класса или интерфейса независимо от установки флагаACC_STATICили от того, принимает ли он аргументы.
Другие методы с именем <clinit> в файле class не являются методами инициализации класса или интерфейса. Они никогда не вызываются самим Java Virtual Machine, не могут быть вызваны никакой инструкцией Java Virtual Machine (§4.9.1) и отклоняются проверкой формата (§4.6, §4.8).
Поскольку имя <clinit> не является допустимым идентификатором в языке программирования Java, его нельзя использовать непосредственно в программе, написанной на языке программирования Java.
Метод является полиморфным по сигнатуре, если все перечисленные ниже условия истинны:
-
Он объявлен в классе
java.lang.invoke.MethodHandleили классеjava.lang.invoke.VarHandle. -
Он имеет один формальный параметр типа
Object[]. -
У него установлены флаги
ACC_VARARGSиACC_NATIVE.
Java Virtual Machine предоставляет специальное обращение к методам с полиморфной сигнатурой в инструкции invokevirtual (§invokevirtual), чтобы осуществить вызов обработчика метода или получить доступ к переменной, на которую ссылается экземпляр java.lang.invoke.VarHandle.
Обработчик метода — это динамически строго типизированная и непосредственно исполняемая ссылка на базовый метод, конструктор, поле или подобную низкоуровневую операцию (§5.4.3.5) с необязательными преобразованиями аргументов или возвращаемых значений. Экземпляр java.lang.invoke.VarHandle — это динамически строго типизированная ссылка на переменную или набор переменных, включая поля static, поля, не являющиеся static, элементы массива или компоненты внеядерной структуры данных. Дополнительную информацию см. в пакете java.lang.invoke в API-интерфейсе Java SE Platform.
Исключение в 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 Марка Фели (Marc Feeley), 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 Virtual Machine состоит из однобайтового кода операции, указывающего операцию, которую необходимо выполнить, за которым следуют ноль или более операндов, предоставляющих аргументы или данные, используемые операцией. Многие инструкции не имеют операндов и состоят только из кода операции.
Игнорируя исключения, внутренний цикл интерпретатора Java Virtual Machine фактически
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 Virtual Machine одним байтом и отказаться от выравнивания данных в скомпилированном коде отражает сознательную ориентацию на компактность, возможно, ценой некоторой производительности в простых реализациях. Однобайтовый код операции также ограничивает размер набора команд. Отсутствие предположения о выравнивании данных означает, что немедленные данные, больше одного байта, должны быть построены из байтов во время выполнения на многих машинах.
Большинство инструкций в наборе команд Java Virtual Machine кодируют информацию о типах выполняемых ими операций. Например, инструкция 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 Virtual Machine в один байт, кодирование типов в коды операций создаёт давление на проектирование набора инструкций. Если каждая типизированная инструкция поддерживала все типы данных времени выполнения Java Virtual Machine, было бы больше инструкций, чем можно представить в одном байте. Вместо этого набор команд Java Virtual Machine обеспечивает сниженный уровень поддержки типов для определённых операций. Другими словами, набор инструкций намеренно не ортогонален. Для преобразования между неподдерживаемыми и поддерживаемыми типами данных, при необходимости, могут быть использованы отдельные инструкции.
Таблица 2.11.1-A обобщает поддержку типов в наборе команд Java Virtual Machine. Конкретная инструкция с информацией о типе создаётся заменой T в шаблоне инструкции в столбце кода операции на букву в столбце типа. Если столбец типа для некоторого шаблона инструкции и типа пуст, то нет инструкции, поддерживающей этот тип операции. Например, есть инструкция загрузки для типа int, iload, но нет инструкции загрузки для типа byte.
Обратите внимание, что большинство инструкций в таблице 2.11.1-A не имеют форм для целочисленных типов byte, char и short. Никакие не имеют форм для типа boolean. Компилятор кодирует загрузку литерных значений типов byte и short с использованием инструкций Java Virtual Machine, которые расширяют эти значения со знаком до значений типа int во время компиляции или выполнения. Загрузки литерных значений типов boolean и char кодируются с использованием инструкций, которые расширяют литерные значения с нулём до значений типа int во время компиляции или выполнения. Аналогично, загрузки из массивов значений типа boolean, byte, short и char кодируются с использованием инструкций Java Virtual Machine, которые расширяют значения со знаком или нулём до значений типа 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, то результатом является значениеlongV. -
Если T имеет тип
intи это целое значение может быть представлено какint, то результатом является значениеintV.
-
-
В противном случае:
-
Значение должно быть слишком малым (отрицательное значение большой величины или отрицательная бесконечность), и результатом является наименьшее представимое значение типа
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.1), или метод текущего класса или его предков.
-
invokestatic вызывает метод класса (
static) в указанном классе. -
invokedynamic вызывает метод, который является целевым объектом вызова, связанного с инструкцией invokedynamic. Объект вызова был привязан к конкретному лексическому появлению инструкции invokedynamic виртуальной машиной Java в результате выполнения метода загрузки перед первым выполнением инструкции. Таким образом, каждое появление инструкции invokedynamic имеет уникальное состояние связи, в отличие от других инструкций, которые вызывают методы.
Инструкции возврата метода, которые различаются по типу возвращаемого значения, — это ireturn (используется для возврата значений типов boolean, byte, char, short или int), lreturn, freturn, dreturn и areturn. Кроме того, инструкция return используется для возврата из методов, объявленных как void, методов инициализации экземпляров и методов инициализации классов или интерфейсов.
Исключение выбрасывается программно с помощью инструкции athrow. Исключения также могут быть сгенерированы различными инструкциями виртуальной машины Java, если они обнаруживают аномальное состояние.
Виртуальная машина Java поддерживает синхронизацию как методов, так и последовательностей инструкций внутри метода с помощью единственной конструкции синхронизации: монитора monitor.
Синхронизация на уровне метода выполняется неявно как часть вызова и возврата метода (§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.