Глава 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, версия 15.0, доступного по адресу https://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 содержит явную поддержку объектов. Объект — это либо динамически выделенный экземпляр класса, либо массив. Ссылка на объект считается имеющей тип виртуальной машины 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, значения которого точно соответствуют значениям, представимым в 32-битном формате IEEE 754 binary32, а значение по умолчанию — положительный ноль -
double, значения которого точно соответствуют значениям 64-битного формата IEEE 754 binary64, а значение по умолчанию — положительный ноль
Значения типа boolean кодируют логические значения true и false, а значение по умолчанию — false.
В первом издании спецификации виртуальной машины Java® тип boolean не рассматривался как тип виртуальной машины Java. Тем не менее, значения boolean имеют ограниченную поддержку в виртуальной машине Java. Во втором издании спецификации виртуальной машины 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, которые концептуально связаны с 32-битным форматом binary32 и 64-битным форматом binary64 для значений и операций с плавающей запятой IEEE 754, как определено в стандарте IEEE 754 (JLS §1.7).
В Java SE 15 и более поздних версиях виртуальная машина Java использует версию стандарта IEEE 754 2019 года. До Java SE 15 виртуальная машина Java использовала версию стандарта IEEE 754 1985 года, где формат binary32 был известен как формат single, а формат binary64 — как формат double.
Стандарт IEEE 754 включает в себя не только положительные и отрицательные числа, состоящие из знака и модуля, но также положительный и отрицательный ноль, положительную и отрицательную бесконечность и специальные значения Not-a-Number (сокращённо NaN). Значение NaN используется для представления результата некоторых недопустимых операций, таких как деление нуля на ноль. Постоянные NaN обоих типов float и double предварительно определены как Float.NaN и Double.NaN.
Конечные ненулевые значения типа с плавающей запятой могут быть представлены в виде s ⋅ m ⋅ 2(e - N + 1), где:
-
s равно +1 или -1,
-
m — положительное целое число, меньшее 2
N, -
e — целое число между Emin = -(2K-1-2) и Emax = 2K-1-1 включительно, и
-
Nи K — параметры, зависящие от типа.
Некоторые значения могут быть представлены в этом виде более чем одним способом. Например, предположим, что значение v типа с плавающей запятой может быть представлено в этом виде с определёнными значениями s, m и e, тогда, если m чётное и e меньше 2K-1, можно уменьшить m вдвое и увеличить e на 1, чтобы получить второе представление для того же значения v.
Представление в этом виде называется нормализованным, если m ≥ 2N-1; в противном случае представление называется субнормальным. Если значение типа с плавающей запятой не может быть представлено таким образом, что m ≥ 2N-1, то это значение называется субнормальным значением, поскольку его величина меньше величины наименьшего нормализованного значения.
Ограничения на параметры N и K (и на полученные параметры Emin и Emax) для float и double суммированы в таблице 2.3.2-A.
Таблица 2.3.2-A. Параметры с плавающей запятой
| Параметр | float | double |
|---|---|---|
N | 24 | 53 |
| K | 8 | 11 |
| Emax | +127 | +1023 |
| Emin | -126 | -1022 |
За исключением NaN, значения с плавающей запятой упорядочены. При упорядочивании от наименьшего к наибольшему они представляют собой отрицательную бесконечность, отрицательные конечные ненулевые значения, положительный и отрицательный ноль, положительные конечные ненулевые значения и положительную бесконечность.
IEEE 754 допускает несколько различных значений NaN для каждого из его форматов binary32 и binary64 с плавающей запятой. Однако платформа Java SE обычно рассматривает значения NaN данного типа с плавающей запятой как сворачивающиеся в одно каноническое значение, и поэтому это описание обычно относится к произвольному значению NaN как к каноническому значению.
Согласно IEEE 754, операция с плавающей запятой с аргументами, не являющимися NaN, может генерировать результат NaN. IEEE 754 определяет набор битовых шаблонов NaN, но не предписывает, какой конкретный битовый шаблон NaN используется для представления результата NaN; это определяется архитектурой аппаратного обеспечения. Разработчик может создавать NaN с различными битовыми шаблонами для кодирования, например, ретроспективной диагностической информации. Эти значения NaN могут быть созданы с помощью методов Float.intBitsToFloat и Double.longBitsToDouble для float и double соответственно. И наоборот, для проверки битовых шаблонов значений NaN можно использовать методы Float.floatToRawIntBits и Double.doubleToRawLongBits для float и double соответственно.
Положительный и отрицательный ноль сравниваются как равные, но существуют другие операции, которые могут их различать; например, деление 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, компилируются с использованием значений типа данных виртуальной машины Java int.
Виртуальная машина Java напрямую поддерживает массивы boolean. Инструкция newarray (§newarray) позволяет создавать массивы boolean. Элементы массивов типа boolean обращаются к и изменяются с помощью инструкций массивов byte baload и bastore (§baload, §bastore).
В реализации виртуальной машины Java от Oracle массивы boolean в языке программирования Java кодируются как массивы виртуальной машины Java byte, используя 8 бит на элемент boolean.
Виртуальная машина Java кодирует компоненты массива boolean, используя 1 для представления true и 0 для представления false. Там, где компиляторы языка программирования Java отображают значения на значения типа виртуальной машины Java 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 виртуальной машины Java является неопределённым. Регистр pc виртуальной машины Java достаточно широк, чтобы содержать 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 может использовать обычные стеки, разговорно называемые «C-стеками», для поддержки native методов (методы, написанные на языке, отличном от языка программирования Java). Стек нативных методов также может использоваться реализацией интерпретатора набора инструкций виртуальной машины Java на языке, таком как C. Реализации виртуальной машины Java, которые не могут загрузить native методы и которые сами не полагаются на обычные стеки, не обязаны предоставлять стеки нативных методов. Если они предоставляются, стеки нативных методов обычно выделяются на каждый поток при создании каждого потока.
Этот спецификация допускает, что стеки нативных методов могут иметь фиксированный размер или динамически расширяться и сокращаться по мере необходимости вычисления. Если стеки нативных методов имеют фиксированный размер, размер каждого стека нативных методов может быть выбран независимо при создании этого стека.
Реализация виртуальной машины Java может предоставить программисту или пользователю управление начальным размером стеков нативных методов, а также, в случае стеков нативных методов переменного размера, управление максимальным и минимальным размерами стека методов.
Следующие исключительные ситуации связаны со стеками нативных методов:
-
Если вычисления в потоке требуют большего стека нативных методов, чем разрешено, виртуальная машина Java выбрасывает исключение
StackOverflowError. -
Если стеки нативных методов могут динамически расширяться, и попытка расширения стека нативных методов, но недостаточно памяти может быть выделена, или если недостаточно памяти может быть выделено для создания начального стека нативных методов для нового потока, виртуальная машина Java выбрасывает исключение
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).
Каждый фрейм (§2.6) содержит стек, работающий по принципу «последним вошел — первым вышел» (LIFO), который называется его операционным стеком. Максимальная глубина операционного стека фрейма определяется на этапе компиляции и указывается вместе с кодом метода, связанного с фреймом (§4.7.3).
Если контекст понятен, мы иногда будем называть операционный стек текущего фрейма просто операционным стеком.
Операционный стек пуст при создании содержащего его фрейма. Виртуальная машина Java предоставляет инструкции для загрузки констант или значений из локальных переменных или полей в операционный стек. Другие инструкции виртуальной машины Java берут операнды из операционного стека, выполняют над ними операции и возвращают результат в операционный стек. Операционный стек также используется для подготовки параметров, передаваемых методам, и для получения результатов методов.
Например, инструкция iadd (§iadd) складывает два значения типа 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 внутри метода приводит к тому, что виртуальная машина Java выбрасывает исключение (§2.10), и это исключение не обрабатывается внутри метода. Выполнение инструкции athrow (§athrow) также приводит к явному выбрасыванию исключения, и если исключение не перехватывается текущим методом, происходит прерывистое завершение вызова метода. Вызов метода, завершающийся прерывисто, никогда не возвращает значение своему вызывающему методу.
Виртуальная машина Java не предписывает никакой конкретной внутренней структуры для объектов.
В некоторых реализациях виртуальной машины Java Oracle ссылка на экземпляр класса — это указатель на указатель, который сам является парой указателей: один на таблицу, содержащую методы объекта, и указатель на Class объект, представляющий тип объекта, а другой — на выделенную из кучи память для данных объекта.
Виртуальная машина Java использует подмножество арифметики с плавающей точкой, определённое в стандарте IEEE 754 (JLS §1.7).
В Java SE 15 и более поздних версиях виртуальная машина Java использует версию стандарта IEEE 754 от 2019 года. В предыдущих версиях Java SE (до Java SE 15) виртуальная машина Java использовала версию стандарта IEEE 754 от 1985 года, где формат binary32 был известен как формат single, а формат binary64 — как формат double.
Многие инструкции виртуальной машины Java для арифметики (§2.11.3) и преобразования типов (§2.11.4) работают с числами с плавающей точкой. Эти инструкции, как правило, соответствуют операциям IEEE 754 (Таблица 2.8-A), за исключением некоторых инструкций, описанных ниже.
Таблица 2.8-A. Соответствие операциям IEEE 754
| Инструкция | Операция IEEE 754 |
|---|---|
| dcmp<op> (§dcmp<op>), fcmp<op> (§fcmp<op>) | compareQuietLess, compareQuietLessEqual, compareQuietGreater, compareQuietGreaterEqual, compareQuietEqual, compareQuietNotEqual |
| dadd (§dadd), fadd (§fadd) | сложение |
| dsub (§dsub), fsub (§fsub) | вычитание |
| dmul (§dmul), fmul (§fmul) | умножение |
| ddiv (§ddiv), fdiv (§fdiv) | деление |
| dneg (§dneg), fneg (§fneg) | изменение знака |
| i2d (§i2d), i2f (§i2f), l2d (§l2d), l2f (§l2f) | преобразование из целого |
| d2i (§d2i), d2l (§d2l), f2i (§f2i), f2l (§f2l) | преобразование в целое число (к нулю) |
| d2f (§d2f), f2d (§f2d) | преобразование формата |
Ключевые отличия арифметики с плавающей точкой, поддерживаемой виртуальной машиной Java, от стандарта IEEE 754:
-
Инструкции остатка с плавающей точкой drem (§drem) и frem (§frem) не соответствуют операции остатка IEEE 754. Эти инструкции основаны на неявном делении с политикой округления к нулю; операция остатка IEEE 754, в свою очередь, основана на неявном делении с политикой округления к ближайшему. (Политики округления обсуждаются ниже.)
-
Инструкции изменения знака с плавающей точкой dneg (§dneg) и fneg (§fneg) не точно соответствуют операции изменения знака IEEE 754. В частности, инструкции не требуют инвертирования бита знака операнда NaN.
-
Инструкции виртуальной машины Java с плавающей точкой не генерируют исключения, не прерываются и не сигнализируют об исключительных ситуациях IEEE 754 (ошибочная операция, деление на ноль, переполнение, недополнение или неточность).
-
Виртуальная машина Java не поддерживает сигнализирующие сравнения чисел с плавающей точкой IEEE 754 и не имеет сигнализирующего значения NaN.
-
IEEE 754 включает атрибуты направления округления, которые не соответствуют политике округления в виртуальной машине Java. Виртуальная машина Java не предоставляет возможности изменения политики округления, используемой заданной инструкцией с плавающей точкой.
-
Виртуальная машина Java не поддерживает расширенные форматы чисел с плавающей точкой binary32 и binary64, определённые в IEEE 754. Нельзя использовать ни расширенный диапазон, ни расширенную точность, превышающие те, которые определены для типов
floatиdoubleпри работе с или хранении значений с плавающей точкой.
Некоторые операции IEEE 754 без соответствующих инструкций в виртуальной машине Java предоставляются через методы в классах Math и StrictMath, включая метод sqrt для операции квадратного корня IEEE 754, метод fma для операции IEEE 754 fusedMultiplyAdd и метод IEEEremainder для операции остатка IEEE 754.
Виртуальная машина Java требует поддержки чисел с плавающей точкой IEEE 754 subnormal и gradual underflow, что упрощает доказательство желаемых свойств некоторых численных алгоритмов.
Арифметика с плавающей точкой представляет собой приближение к действительной арифметике. Хотя существует бесконечное число действительных чисел, у конкретного формата чисел с плавающей точкой лишь конечное число значений. В виртуальной машине Java политика округления — это функция, используемая для отображения действительного числа в значение с плавающей точкой в данном формате. Для действительных чисел в диапазоне представления формата чисел с плавающей точкой непрерывный отрезок числовой оси действительных чисел отображается в одно значение с плавающей точкой. Действительное число, численно равное значению с плавающей точкой, отображается в это значение с плавающей точкой; например, действительное число 1,5 отображается в значение с плавающей точкой 1,5 в данном формате. Виртуальная машина Java определяет две политики округления следующим образом:
-
Правило округления до ближайшего целого применяется ко всем операциям с плавающей точкой, за исключением (i) преобразования в целое значение и (ii) вычисления остатка. При округлении до ближайшего целого значения, неточные результаты должны быть округлены до ближайшего представимого значения к бесконечно точному результату; если два ближайших представимых значения равноудалены, то выбирается значение, младший бит которого равен нулю.
Правило округления до ближайшего целого соответствует атрибуту направления округления по умолчанию для двоичной арифметики в стандарте IEEE 754, roundTiesToEven.
Атрибут направления округления roundTiesToEven был известен как режим округления «до ближайшего целого» в версии стандарта IEEE 754 от 1985 года. Правило округления в Java Virtual Machine названо в честь этого режима округления.
-
Правило округления к нулю применяется к (i) преобразованию значения с плавающей точкой в целое значение с помощью инструкций d2i, d2l, f2i и f2l (§d2i, §d2l, §f2i, §f2l), и (ii) инструкциям вычисления остатка с плавающей точкой drem и frem (§drem, §frem). При округлении к нулю неточные результаты округляются до ближайшего представимого значения, модуль которого не больше, чем модуль бесконечно точного результата. Для преобразования в целое, правило округления к нулю эквивалентно усечению, где дробные биты мантиссы отбрасываются.
Правило округления к нулю соответствует атрибуту направления округления roundTowardZero для двоичной арифметики в стандарте IEEE 754.
Атрибут направления округления roundTowardZero был известен как режим округления «к нулю» в версии стандарта IEEE 754 от 1985 года. Правило округления в Java Virtual Machine названо в честь этого режима округления.
Java Virtual Machine требует, чтобы каждая инструкция с плавающей точкой округляла свой результат до точности результата. Правило округления, используемое каждой инструкцией, это либо округление до ближайшего целого, либо округление к нулю, как указано выше.
Java 1.0 и 1.1 требовали строгого вычисления выражений с плавающей точкой. Строгое вычисление означает, что каждый float операнд соответствует значению, представимому в формате IEEE 754 binary32, каждый double операнд соответствует значению, представимому в формате IEEE 754 binary64, и каждая операция с плавающей точкой с соответствующей операцией IEEE 754 соответствует результату IEEE 754 для тех же операндов.
Строгое вычисление обеспечивает предсказуемые результаты, но вызывало проблемы производительности в реализациях Java Virtual Machine для некоторых семейств процессоров, распространённых в эпоху Java 1.0/1.1. Вследствие этого, в Java 1.2 до Java SE 16, платформа Java SE разрешала реализации Java Virtual Machine иметь один или два множества значений для каждого типа с плавающей точкой. Тип float был связан с множеством значений с плавающей точкой и множеством значений с плавающей точкой и расширенным порядком, а тип double был связан с множеством значений с двойной точностью и множеством значений с двойной точностью и расширенным порядком. Множество значений с плавающей точкой соответствовало значениям, представимым в формате IEEE 754 binary32; множество значений с плавающей точкой и расширенным порядком имело такое же количество разрядов точности, но больший диапазон порядка. Аналогично, множество значений с двойной точностью соответствовало значениям, представимым в формате IEEE 754 binary64; множество значений с двойной точностью и расширенным порядком имело такое же количество разрядов точности, но больший диапазон порядка. Разрешение использования множеств значений с расширенным порядком по умолчанию смягчило проблемы производительности на некоторых семействах процессоров.
Для совместимости, Java 1.2 разрешало class файл, чтобы запретить реализации использовать множества значений с расширенным порядком. class файл выражал это, устанавливая флаг ACC_STRICT в объявлении метода. ACC_STRICT ограничивало семантику операций с плавающей точкой в инструкциях метода, используя множество значений с плавающей точкой для float операндов и множество значений с двойной точностью для double операндов, гарантируя, что результаты таких инструкций были полностью предсказуемы. Методы, помеченные как ACC_STRICT, таким образом, имели ту же семантику операций с плавающей точкой, что и в Java 1.0 и 1.1.
В Java SE 17 и более поздних версиях платформа Java SE всегда требует строгого вычисления выражений с плавающей точкой. Новые члены семейств процессоров, которые имели проблемы с реализацией строгого вычисления, больше не испытывают этих трудностей. Это спецификация больше не связывает float и double с четырьмя множествами значений, описанных выше, и флаг ACC_STRICT больше не влияет на вычисление операций с плавающей точкой. Для совместимости, битовая последовательность, назначенная для обозначения ACC_STRICT в файле class с номером основной версии 46-60, не используется (то есть не обозначает никакого флага) в файле class с номером основной версии больше 60 (§4.6). Будущие версии Java Virtual Machine могут назначить другое значение битовой последовательности в будущих файлах class.
Класс может иметь ноль или более методов инициализации экземпляра, каждый из которых обычно соответствует конструктору, написанному на языке программирования 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.
Исключение в Java Virtual Machine представляется экземпляром класса Throwable или одного из его подклассов. Бросание исключения приводит к немедленному нелокальному переходу управления от точки, в которой было брошено исключение.
Большинство исключений возникают синхронно в результате действия потока, в котором они происходят. В отличие от этого, асинхронное исключение может потенциально возникнуть в любой момент выполнения программы. Java Virtual Machine бросает исключение по одной из трех причин:
-
Была выполнена инструкция athrow (§athrow).
-
Синхронно было обнаружено аномальное условие выполнения Java Virtual Machine. Такие исключения не выбрасываются в произвольной точке программы, а только синхронно после выполнения инструкции, которая либо:
-
Указывает исключение как возможный результат, например:
-
Когда инструкция реализует операцию, нарушающую семантику языка программирования Java, например, индексирование за пределами границ массива.
-
Когда происходит ошибка при загрузке или линковке части программы.
-
-
Приводит к превышению какого-либо ограничения ресурса, например, при использовании слишком большого объема памяти.
-
-
Произошло асинхронное исключение из-за внутренней ошибки в реализации Java Virtual Machine (§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). При внезапном завершении стек операндов и локальные переменные текущего вызова метода отбрасываются, а его кадр удаляется, восстанавливая кадр вызывающего метода. Затем исключение повторно бросается в контексте кадра вызывающего метода и так далее, продолжая по цепочке вызовов методов. Если подходящий обработчик не найден до тех пор, пока не будет достигнута вершина цепочки вызовов методов, выполнение потока, в котором было брошено исключение, завершается. Перед завершением потока необработанное исключение обрабатывается в соответствии со следующими правилами:
-
Если в потоке установлен обработчик необработанных исключений, то этот обработчик выполняется.
-
В противном случае вызывается метод
uncaughtExceptionдляThreadGroup, который является родителем потока. ЕслиThreadGroupи его родительскиеThreadGroupне переопределяютuncaughtException, то вызывается методuncaughtExceptionобработчика по умолчанию.
Порядок, в котором ищутся обработчики исключений метода, важен. Внутри файле 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 не указывает переполнение или недополнение при операциях над числами с плавающей точкой. То есть, инструкции с плавающей точкой никогда не заставляют виртуальную машину 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, также сохраняют числовое значение точно.
Преобразования от int к float, или от long к float, или от long к double, могут терять точность, то есть могут терять некоторые младшие биты значения; полученное значение с плавающей точкой является корректно округленной версией целочисленного значения, используя политику округления до ближайшего значения (§2.8).
Несмотря на то, что может произойти потеря точности, расширяющие числовые преобразования никогда не вызывают в виртуальной машине 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 с помощью политики округления к нулю (§2.8). Есть два случая:
-
Если T —
long, и это целое значение может быть представлено какlong, то результатом является значениеlongV. -
Если T — типа
int, и это целое значение может быть представлено какint, то результатом является значениеintV.
-
-
В противном случае:
-
Значение должно быть слишком малым (отрицательное значение большой величины или отрицательная бесконечность), и результатом является наименьшее представимое значение типа
intилиlong. -
Или значение должно быть слишком большим (положительное значение большой величины или положительная бесконечность), и результатом является наибольшее представимое значение типа
intилиlong.
-
Сужающее числовое преобразование от double к float выполняется в соответствии с IEEE 754. Результат корректно округляется с использованием политики округления до ближайшего значения (§2.8). Значение, слишком малое для представления как 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 поддерживает синхронизацию как методов, так и последовательностей инструкций в методе с помощью единственного конструкта синхронизации: монитор.
Синхронизация на уровне метода выполняется неявно, как часть вызова и возврата метода (§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.