Глава 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, Версия 13.0, доступный по адресу https://www.unicode.org/.
Компилированный код для выполнения виртуальной машиной Java представлен в независимом от оборудования и операционной системы двоичном формате, обычно (но не обязательно) хранящемся в файле, известном как формат файла class. Формат файла class точно определяет представление класса или интерфейса, включая такие детали, как порядок байтов, которые могут быть приняты как должное в формате объектного файла, специфичном для платформы.
В главе 4 "Формат файла class" подробно рассматривается формат файла class.
Как и язык программирования Java, виртуальная машина Java работает с двумя видами типов: примитивные типы и ссылочные типы. Соответственно, существуют два вида значений, которые могут храниться в переменных, передаваться в качестве аргументов, возвращаться методами и обрабатываться: примитивные значения и ссылочные значения.
Виртуальная машина Java ожидает, что практически вся проверка типов выполняется до времени выполнения, обычно компилятором, и не должна выполняться самой виртуальной машиной. Значения примитивных типов не обязательно должны быть помечены или каким-либо образом проверяемы на тип во время выполнения, или отличаться от значений ссылочных типов. Вместо этого, набор инструкций виртуальной машины Java различает типы операндов с помощью инструкций, предназначенных для работы со значениями определенных типов. Например, iadd, ladd, fadd и dadd — это инструкции виртуальной машины Java, которые складывают два числовых значения и производят числовой результат, но каждая специализируется на своём типе операнда: int, long, float и double соответственно. Сводка поддержки типов в наборе инструкций виртуальной машины Java приведена в §2.11.1.
Виртуальная машина Java содержит явную поддержку объектов. Объект — это либо динамически выделенный экземпляр класса, либо массив. Ссылка на объект считается имеющей тип виртуальной машины Java reference. Значения типа reference можно рассматривать как указатели на объекты. Может существовать более одной ссылки на один объект. Объекты всегда обрабатываются, передаются и проверяются с помощью значений типа reference.
Примитивные типы данных, поддерживаемые виртуальной машиной Java, — это числовые типы, тип boolean (§2.3.4) и тип returnAddress (§2.3.3).
Числовые типы состоят из целочисленных типов (§2.3.1) и типов с плавающей точкой (§2.3.2).
Целочисленные типы:
-
byte, значениями которого являются 8-битные целые числа со знаком в дополнительном коде, а значение по умолчанию равно нулю -
short, значениями которого являются 16-битные целые числа со знаком в дополнительном коде, а значение по умолчанию равно нулю -
int, значениями которого являются 32-битные целые числа со знаком в дополнительном коде, а значение по умолчанию равно нулю -
long, значениями которого являются 64-битные целые числа со знаком в дополнительном коде, а значение по умолчанию равно нулю -
char, значениями которого являются 16-битные целые без знака, представляющие коды Юникода базовой многоязычной плоскости, закодированные в UTF-16, а значение по умолчанию — нулевой код ('\u0000')
Типы с плавающей точкой:
-
float, значения которого точно соответствуют значениям, представимым в 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, компиляторы должны использовать такое же кодирование.
Существует три типа ссылочных типов: типы классов, типы массивов и типы интерфейсов. Их значения являются ссылками на динамически созданные экземпляры классов, массивы или экземпляры классов или массивы, соответственно, реализующие интерфейсы.
Тип массива состоит из компонентного типа с одной размерностью (длина которой не задана типом). Компонентный тип массива сам может быть типом массива. Если, начиная с любого типа массива, рассматривать его компонентный тип, а затем (если это также тип массива) компонентный тип этого типа и так далее, то в конечном итоге необходимо достигнуть компонентного типа, который не является типом массива; это называется типом элемента типа массива. Тип элемента типа массива обязательно является либо примитивным типом, либо типом класса, либо типом интерфейса.
Значение ссылочного типа также может быть специальной нулевой ссылкой, ссылкой на ни один объект, которая здесь будет обозначаться как null. Ссылка на null изначально не имеет типа во время выполнения, но может быть приведена к любому типу. Значением по умолчанию для ссылочного типа является null.
В данном спецификации не задаётся конкретное кодирование значений null.
Виртуальная машина Java определяет различные области данных во время выполнения, которые используются во время выполнения программы. Некоторые из этих областей данных создаются при запуске виртуальной машины Java и уничтожаются только при выходе виртуальной машины Java. Другие области данных относятся к каждому потоку. Области данных, относящиеся к каждому потоку, создаются при создании потока и уничтожаются при завершении потока.
Виртуальная машина Java может поддерживать множество потоков выполнения одновременно (JLS §17). Каждый поток виртуальной машины Java имеет свой собственный регистр pc (счётчик команд). В любой момент каждый поток виртуальной машины Java выполняет код одного метода, а именно текущего метода (§2.6) для этого потока. Если этот метод не native, регистр pc содержит адрес инструкции виртуальной машины, которая выполняется в данный момент. Если метод, который выполняется потоком в настоящий момент, является native, значение регистра pc виртуальной машины не определено. Регистр pc виртуальной машины достаточно широк, чтобы содержать returnAddress или указатель нативный указатель на конкретной платформе.
Каждый поток виртуальной машины Java имеет свой собственный частный стек виртуальной машины Java, созданный одновременно с потоком. Стек виртуальной машины Java хранит фреймы (§2.6). Стек виртуальной машины Java аналогичен стеку традиционного языка, такого как C: он хранит локальные переменные и частичные результаты, и играет роль в вызове и возвращении методов. Поскольку стек виртуальной машины Java никогда не манипулируется напрямую, кроме как для помещения и извлечения фреймов, фреймы могут быть размещены в куче. Память для стека виртуальной машины Java не обязательно должна быть непрерывной.
В первом издании спецификации Виртуальной машины Java® стек виртуальной машины Java был известен как стек Java.
Данная спецификация разрешает стекам виртуальной машины Java либо иметь фиксированный размер, либо динамически расширяться и сжиматься по мере необходимости для вычисления. Если стеки виртуальной машины Java имеют фиксированный размер, размер каждого стека виртуальной машины Java может быть выбран независимо при создании этого стека.
Реализация виртуальной машины Java может предоставить программисту или пользователю управление начальным размером стеков виртуальной машины Java, а также, в случае динамического расширения или сжатия стеков виртуальной машины Java, управление максимальным и минимальным размерами.
Следующие исключительные ситуации связаны со стеком виртуальной машины Java:
-
Если вычисление в потоке требует стека виртуальной машины Java больше, чем разрешено, виртуальная машина Java генерирует исключение
StackOverflowError. -
Если стеки виртуальной машины Java могут динамически расширяться, а попытка расширения невозможна из-за нехватки памяти, или если недостаточно памяти для создания начального стека виртуальной машины Java для нового потока, виртуальная машина Java генерирует исключение
OutOfMemoryError.
Виртуальная машина Java имеет кучу, которая используется всеми потоками виртуальной машины Java. Куча является областью данных во время выполнения, из которой выделяется память для всех экземпляров классов и массивов.
Куча создаётся при запуске виртуальной машины. Хранение объектов в куче восстанавливается системой автоматического управления памятью (известной как сборщик мусора); объекты никогда не освобождаются явно. Виртуальная машина Java не предполагает конкретный тип системы автоматического управления памятью, а техника управления памятью может быть выбрана в соответствии с потребностями системы реализатора. Куча может иметь фиксированный размер или может расширяться по мере необходимости для вычисления и может сжиматься, если большая куча становится ненужной. Память для кучи не обязательно должна быть непрерывной.
Реализация виртуальной машины Java может предоставить программисту или пользователю управление начальным размером кучи, а также, если куча может динамически расширяться или сжиматься, управление максимальным и минимальным размером кучи.
Следующее исключительное условие связано с кучей:
-
Если вычисление требует больше памяти кучи, чем может предоставить система автоматического управления памятью, виртуальная машина Java генерирует исключение
OutOfMemoryError.
Виртуальная машина Java имеет область методов, которая используется всеми потоками виртуальной машины Java. Область методов аналогична области хранения скомпилированного кода традиционного языка или аналогична сегменту «текст» в процессе операционной системы. Она хранит структуры для каждого класса, такие как пул постоянных во время выполнения, данные о полях и методах, и код для методов и конструкторов, включая специальные методы, используемые при инициализации класса и интерфейса и при инициализации экземпляра (§2.9).
Область методов создаётся при запуске виртуальной машины. Хотя область методов логически является частью кучи, простые реализации могут не собирать мусор или не компактировать её. Данная спецификация не требует размещения области методов или политик, используемых для управления скомпилированным кодом. Область методов может иметь фиксированный размер или может расширяться по мере необходимости для вычисления и может сжиматься, если большая область методов становится ненужной. Память для области методов не обязательно должна быть непрерывной.
Реализация виртуальной машины Java может предоставить программисту или пользователю управление начальным размером области методов, а также, в случае области методов с изменяемым размером, управление максимальным и минимальным размером области методов.
Следующее исключительное условие связано с областью методов:
-
Если память в области методов недоступна для удовлетворения запроса на выделение памяти, виртуальная машина Java генерирует исключение
OutOfMemoryError.
Пул постоянных во время выполнения — это представление во время выполнения для каждого класса или интерфейса таблицы constant_pool в файле class (§4.4). Он содержит несколько видов констант, начиная от числовых литералов, известных на этапе компиляции, до ссылок на методы и поля, которые должны быть разрешены во время выполнения. Пул постоянных во время выполнения выполняет функцию, аналогичную таблице символов для обычного языка программирования, хотя он содержит более широкий спектр данных, чем типичная таблица символов.
Каждый пул постоянных во время выполнения выделяется из области методов виртуальной машины Java (§2.5.4). Пул постоянных во время выполнения для класса или интерфейса создаётся при создании класса или интерфейса (§5.3) виртуальной машиной Java.
Следующее исключительное условие связано с построением пула постоянных во время выполнения для класса или интерфейса:
-
При создании класса или интерфейса, если для построения пула постоянных во время выполнения требуется больше памяти, чем доступно в области методов виртуальной машины Java, виртуальная машина Java генерирует исключение
OutOfMemoryError.
См. §5 (Загрузка, привязка и инициализация) для получения информации о построении пула постоянных во время выполнения.
Реализация виртуальной машины Java может использовать обычные стеки, разговорно называемые «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, например, индексирование за пределами границ массива.
-
Когда возникает ошибка при загрузке или связывании части программы.
-
-
Приводит к превышению некоторого ограничения на ресурс, например, при использовании слишком большого объёма памяти.
-
-
Возникло асинхронное исключение, потому что:
-
Был вызван метод
stopклассаThreadилиThreadGroup, или -
Произошла внутренняя ошибка в реализации Java Virtual Machine.
Методы
stopмогут быть вызваны одним потоком для воздействия на другой поток или все потоки в указанной группе потоков. Они являются асинхронными, потому что могут произойти в любой момент выполнения других потоков. Внутренняя ошибка считается асинхронной (§6.3). -
Java Virtual Machine может разрешить выполнение небольшого, но ограниченного объёма кода перед броском асинхронного исключения. Это задержка допускается для того, чтобы оптимизированный код мог обнаружить и бросить эти исключения в точках, где их практично обработать, при соблюдении семантики языка программирования Java.
Простая реализация могла бы проверять наличие асинхронных исключений в точке каждой инструкции перехода управления. Поскольку программа имеет конечный размер, это обеспечивает ограничение на общую задержку при обнаружении асинхронного исключения. Поскольку асинхронное исключение не возникнет между переходами управления, генератор кода имеет некоторую гибкость для переупорядочения вычислений между переходами управления для повышения производительности. Рекомендуется для дальнейшего изучения статья Polling Efficiently on Stock Hardware Марка Фели, Proc. 1993 Conference on Functional Programming and Computer Architecture, Копенгаген, Дания, стр. 179–187.
Исключения, выброшенные Java Virtual Machine, являются точными: когда происходит переход управления, все эффекты инструкций, выполненных до точки, из которой выбрасывается исключение, должны казаться выполненными. Никакие инструкции, которые появляются после точки, из которой выбрасывается исключение, не могут казаться оцененными. Если оптимизированный код спекулятивно выполнил некоторые из инструкций, следующие за точкой, в которой возникает исключение, такой код должен быть готов скрыть это спекулятивное выполнение от состояния программы, видимого пользователю.
Каждый метод в Java Virtual Machine может быть связан с нулем или более обработчиками исключений. Обработчик исключений задаёт диапазон смещений в коде Java Virtual Machine, реализующем метод, для которого активен обработчик исключений, описывает тип исключения, которое может обработать обработчик исключений, и указывает расположение кода, который должен обработать это исключение. Исключение соответствует обработчику исключений, если смещение инструкции, вызвавшей исключение, находится в диапазоне смещений обработчика исключений, и тип исключения — тот же класс, что и класс исключения, или подкласс класса исключения, который обрабатывает обработчик исключений. Когда происходит выброс исключения, Java Virtual Machine ищет соответствующий обработчик исключений в текущем методе. Если такой обработчик исключений найден, система переходит к коду обработки исключения, указанному соответствующим обработчиком.
Если такой обработчик исключений не найден в текущем методе, текущее вызов метода завершается внезапно (§2.6.5). При внезапном завершении стек операндов и локальные переменные текущего вызова метода удаляются, а его фрейм извлекается, восстанавливая фрейм вызывающего метода. Затем исключение повторно бросается в контексте фрейма вызывающего метода и так далее, продолжая вверх по цепочке вызовов методов. Если подходящий обработчик исключений не найден до достижения вершины цепочки вызовов методов, выполнение потока, в котором было выброшено исключение, завершается.
Порядок, в котором ищутся обработчики исключений метода, важен. Внутри файла class обработчики исключений каждого метода хранятся в таблице (§4.7.3). Во время выполнения, когда происходит выброс исключения, Java Virtual Machine ищет обработчики исключений текущего метода в порядке их появления в соответствующей таблице обработчиков исключений в файле class, начиная с начала этой таблицы.
Обратите внимание, что Java Virtual Machine не навязывает вложение или какой-либо порядок записей таблицы исключений метода. Семантика обработки исключений языка программирования Java реализуется только за счёт сотрудничества с компилятором (§3.12). Когда файлы class генерируются другими средствами, определённая процедура поиска гарантирует, что все реализации Java Virtual Machine будут вести себя согласованно.
Инструкция Java Virtual Machine состоит из одного байта opcode, определяющего операцию, которую нужно выполнить, за которым следуют ноль или более операндов, предоставляющих аргументы или данные, используемые операцией. Многие инструкции не имеют операндов и состоят только из opcode.
Игнорируя исключения, внутренний цикл интерпретатора 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);
Количество и размер операндов определяются opcode. Если операнд больше одного байта, он хранится в порядке big-endian - старший байт первым. Например, 16-битный беззнаковый индекс в локальных переменных хранится как два беззнаковых байта, byte1 и byte2, так что его значение равно (byte1 << 8) | byte2.
Поток байткода инструкций выровнен только по одному байту. Два исключения — инструкции lookupswitch и tableswitch (§lookupswitch, §tableswitch), которые дополняются для принудительного внутреннего выравнивания некоторых их операндов по 4-байтовым границам.
Решение ограничить opcode Java Virtual Machine одним байтом и отказаться от выравнивания данных в скомпилированном коде отражает сознательный уклон в пользу компактности, возможно, ценой некоторой производительности в простых реализациях. Один байт opcode также ограничивает размер набора инструкций. Не предполагая выравнивания данных, непосредственные данные, превышающие один байт, должны быть построены из байтов во время выполнения на многих машинах.
Большинство инструкций в наборе инструкций Java Virtual Machine кодируют информацию о типах выполняемых ими операций. Например, инструкция iload (§iload) загружает содержимое локальной переменной, которая должна быть int, на стек операндов. Инструкция fload (§fload) делает то же самое со значением типа float. Обе инструкции могут иметь идентичные реализации, но имеют разные opcode.
Для большинства типизированных инструкций тип инструкции явно представлен в мнемонике opcode буквой: i для int операции, l для long, s для short, b для byte, c для char, f для float, d для double и a для reference. Некоторые инструкции, для которых тип недвусмысленен, не имеют буквы типа в своей мнемонике. Например, arraylength всегда работает с объектом, являющимся массивом. Некоторые инструкции, такие как goto, безусловный переход управления, не работают с типизированными операндами.
Учитывая размер opcode Java Virtual Machine в один байт, кодирование типов в opcode оказывает давление на проектирование набора инструкций. Если каждая типизированная инструкция поддерживала все типы данных времени выполнения Java Virtual Machine, было бы больше инструкций, чем можно представить в одном байте. Вместо этого, набор инструкций Java Virtual Machine обеспечивает пониженный уровень поддержки типов для определенных операций. Другими словами, набор инструкций намеренно не ортогонален. Для преобразования между неподдерживаемыми и поддерживаемыми типами данных, по необходимости, могут использоваться отдельные инструкции.
Таблица 2.11.1-A обобщает поддержку типов в наборе инструкций Java Virtual Machine. Конкретная инструкция с информацией о типе создается путём замены T в шаблоне инструкции в столбце opcode буквой в столбце типа. Если столбец типа для некоторого шаблона инструкции и типа пуст, то никакой инструкции, поддерживающей этот тип операции, не существует. Например, существует инструкция загрузки для типа 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, преобразуется в положительную или отрицательную бесконечность. NaN типа double всегда преобразуется в NaN типа float.
Несмотря на то, что могут произойти переполнение, недополнение или потеря точности, сужающие преобразования между числовыми типами никогда не заставляют виртуальную машину 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.