Глава 2. Структура виртуальной машины Java
Оглавление
- 2.1. Формат файла
class - 2.2. Типы данных
- 2.3. Примитивные типы и значения
- 2.4. Ссылочные типы и значения
- 2.5. Области данных во время выполнения
- 2.6. Фреймы
- 2.7. Представление объектов
- 2.8. Арифметика с плавающей точкой
- 2.9. Специальные методы
- 2.10. Исключения
- 2.11. Краткий обзор набора команд
-
- 2.11.1. Типы и виртуальная машина Java
- 2.11.2. Команды загрузки и сохранения
- 2.11.3. Арифметические команды
- 2.11.4. Команды преобразования типов
- 2.11.5. Создание и обработка объектов
- 2.11.6. Команды управления стеком операндов
- 2.11.7. Команды передачи управления
- 2.11.8. Команды вызова и возврата методов
- 2.11.9. Генерация исключений
- 2.11.10. Синхронизация
- 2.12. Библиотеки классов
- 2.13. Публичный дизайн, приватная реализация
Этот документ описывает абстрактную машину. Он не описывает никакой конкретной реализации виртуальной машины Java.
Для корректной реализации виртуальной машины Java вам необходимо только уметь читать формат файла class и правильно выполнять в нём указанные операции. Детали реализации, которые не являются частью спецификации виртуальной машины Java, излишне ограничат творческий потенциал разработчиков. Например, расположение в памяти областей данных во время выполнения, используемый алгоритм сборки мусора и любые внутренние оптимизации команд виртуальной машины Java (например, их перевод в машинный код) оставляются на усмотрение разработчика.
Все ссылки на Unicode в этой спецификации даются относительно Стандарта Unicode, версия 6.0.0, доступного по адресу http://www.unicode.org/.
Скомпилированный код, который должен выполняться виртуальной машиной Java, представлен в формате, независимом от аппаратной платформы и операционной системы, обычно (но необязательно) хранящемся в файле, известном как формат файла class. Формат файла class точно определяет представление класса или интерфейса, включая такие детали, как порядок байтов, которые могут быть приняты как само собой разумеющиеся в формате платформенно-зависимого объектного файла.
Глава 4, "Формат файла class", подробно рассматривает формат файла class.
Подобно языку программирования Java, виртуальная машина Java работает с двумя видами типов: примитивными типами и ссылочными типами. Соответственно, есть два вида значений, которые могут храниться в переменных, передаваться в качестве аргументов, возвращаться методами и обрабатываться: примитивные значения и ссылочные значения.
Виртуальная машина Java ожидает, что почти вся проверка типов выполняется до времени выполнения, обычно компилятором, и не должна выполняться самой виртуальной машиной. Значения примитивных типов не должны быть помечены или каким-либо другим образом инспектируемы, чтобы определить их типы во время выполнения или отличить их от значений ссылочных типов. Вместо этого набор команд виртуальной машины Java различает типы операндов, используя команды, предназначенные для работы со значениями определённых типов. Например, iadd, ladd, fadd и dadd — это команды виртуальной машины Java, которые складывают два числовых значения и производят числовые результаты, но каждая из них специализируется на своём типе операндов: int, long, float и double соответственно. Сводку поддержки типов в наборе команд виртуальной машины Java см. в §2.11.1.
Виртуальная машина Java содержит явную поддержку объектов. Объект — это либо динамически выделенный экземпляр класса, либо массив. Ссылка на объект рассматривается как имеющая тип виртуальной машины Java reference. Значения типа reference можно рассматривать как указатели на объекты. Может существовать более одной ссылки на объект. Объекты всегда обрабатываются, передаются и проверяются с помощью значений типа reference.
Примитивные типы данных, поддерживаемые виртуальной машиной Java, — это числовые типы, тип
boolean (§2.3.4) и тип returnAddress (§2.3.3).
Числовые типы состоят из целочисленных типов (§2.3.1) и типов с плавающей точкой (§2.3.2).
Целочисленные типы:
-
byte, значения которого — целые 8-битные числа со знаком в дополнительном коде, а значение по умолчанию — ноль -
short, значения которого — целые 16-битные числа со знаком в дополнительном коде, а значение по умолчанию — ноль -
int, значения которого — целые 32-битные числа со знаком в дополнительном коде, а значение по умолчанию — ноль -
long, значения которого — целые 64-битные числа со знаком в дополнительном коде, а значение по умолчанию — ноль -
char, значения которого — 16-битные беззнаковые целые числа, представляющие кодовые точки Юникода в базовой многоязычной плоскости, закодированные в UTF-16, а значение по умолчанию — нулевая кодовая точка ('\u0000')
Типы с плавающей точкой:
-
float, значения которого — элементы множества значений float или, если поддерживается, множества значений float с расширенным порядком, а значение по умолчанию — положительный ноль -
double, значения которого — элементы множества значений double или, если поддерживается, множества значений double с расширенным порядком, а значение по умолчанию — положительный ноль
Значения типа boolean кодируют значения истинности true и false, а значение по умолчанию — false.
Спецификация виртуальной машины Java, первое издание не рассматривала 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, которые концептуально связаны со значениями и операциями формата IEEE 754 с одинарной точностью (32 бита) и двойной точностью (64 бита), как указано в Стандарте IEEE для арифметики с плавающей запятой (ANSI/IEEE Std. 754-1985, Нью-Йорк).
Стандарт IEEE 754 включает не только положительные и отрицательные числа со знаком и абсолютной величиной, но также положительный и отрицательный ноль, положительную и отрицательную бесконечность и специальное значение «Не число» (сокращённо «NaN»). Значение NaN используется для представления результата некоторых недопустимых операций, таких как деление нуля на ноль.
Каждая реализация виртуальной машины Java обязана поддерживать два стандартных набора значений с плавающей точкой, называемых множеством значений float и множеством значений double. Кроме того, реализация виртуальной машины Java может по своему выбору поддерживать один или оба расширенных набора значений с плавающей точкой с расширенным порядком, называемых множеством значений float с расширенным порядком и множеством значений double с расширенным порядком. Эти наборы значений с расширенным порядком могут в определённых обстоятельствах использоваться вместо стандартных наборов значений для представления значений типа float или double.
Все конечные ненулевые значения любого набора значений с плавающей точкой могут быть выражены в виде s ⋅ m ⋅ 2(e − N + 1), где s равно +1 или −1, m — положительное целое число, меньшее 2N, а e — целое число от Emin = −(2K−1−2) до Emax = 2K−1−1 включительно, где N и K — параметры, зависящие от набора значений. Некоторые значения могут быть представлены в этом виде более чем одним способом; например, предположим, что значение v в наборе значений может быть представлено в этом виде с использованием определённых значений для s, m и e, затем, если m чётное, а e меньше 2K-1, можно уменьшить m вдвое и увеличить e на 1, чтобы получить второе представление для того же значения v. Представление в этом виде называется нормализованным, если m ≥ 2N-1; в противном случае представление называется денормализованным. Если значение в наборе значений не может быть представлено таким образом, что m ≥ 2N-1, то это значение называется денормализованным значением, потому что оно не имеет нормализованного представления.
Ограничения на параметры N и K (и на производные параметры Emin и Emax) для двух необходимых и двух необязательных наборов значений с плавающей точкой приведены в таблице 2.1.
Таблица 2.1. Параметры наборов значений с плавающей точкой
| Параметр | float | float с расширенным порядком | double | double с расширенным порядком |
|---|---|---|---|---|
| N | 24 | 24 | 53 | 53 |
| K | 8 | ≥ 11 | 11 | ≥ 15 |
| Emax | +127 | ≥ +1023 | +1023 | ≥ +16383 |
| Emin | -126 | ≤ -1022 | -1022 | ≤ -16382 |
В случае поддержки одной или обеих расширенных наборов значений с расширенным порядком реализацией, для каждого поддерживаемого набора значений с расширенным порядком существует определённая зависимая от реализации константа K, значение которой ограничено таблицей 2.1; это значение K в свою очередь определяет значения для Emin и Emax.
Каждый из четырёх наборов значений включает не только конечные ненулевые значения, указанные выше, но также пять значений: положительный ноль, отрицательный ноль, положительная бесконечность, отрицательная бесконечность и NaN.
Обратите внимание, что ограничения в таблице 2.1 разработаны таким образом, чтобы каждый элемент набора значений float обязательно был также элементом набора значений float с расширенным порядком, набора значений double и набора значений double с расширенным порядком. Аналогично, каждый элемент набора значений double обязательно также является элементом набора значений double с расширенным порядком. Каждый набор значений с расширенным порядком имеет больший диапазон значений показателя степени, чем соответствующий стандартный набор значений, но не имеет большей точности.
Элементы набора значений float — это точно те значения, которые могут быть представлены с использованием формата с плавающей точкой одинарной точности, определённого в стандарте IEEE 754, за исключением того, что существует только одно значение NaN (стандарт IEEE 754 определяет 224-2 различных значений NaN). Элементы набора значений double — это точно те значения, которые могут быть представлены с использованием формата с плавающей точкой двойной точности, определённого в стандарте IEEE 754, за исключением того, что существует только одно значение NaN (стандарт IEEE 754 определяет 253-2 различных значений NaN). Однако следует отметить, что элементы наборов значений float с расширенным порядком и double с расширенным порядком, определённые здесь, не соответствуют значениям, которые могут быть представлены с использованием форматов IEEE 754 с расширенной одинарной и расширенной двойной точностью соответственно. Этот документ не предписывает конкретное представление для значений наборов значений с плавающей точкой, за исключением случаев, когда значения с плавающей точкой должны быть представлены в формате файла class (§4.4.4, §4.4.5).
Наборы значений float, float с расширенным порядком, double и double с расширенным порядком — это не типы. Для реализации виртуальной машины Java всегда верно использовать элемент набора значений float для представления значения типа float; однако в определённых контекстах реализация может использовать элемент набора значений float с расширенным порядком вместо этого. Аналогично, для реализации всегда верно использовать элемент набора значений double для представления значения типа double; однако в определённых контекстах реализация может использовать элемент набора значений double с расширенным порядком вместо этого.
За исключением NaN, значения наборов значений с плавающей точкой упорядочены. При упорядочении от наименьшего к наибольшему они будут: отрицательная бесконечность, отрицательные конечные значения, положительный и отрицательный ноль, положительные конечные значения и положительная бесконечность.
Положительный и отрицательный ноль с плавающей точкой сравниваются как равные, но есть другие операции, которые могут их отличить; например, деление 1.0 на 0.0 даёт положительную бесконечность, а деление 1.0 на -0.0 даёт отрицательную бесконечность.
NaN не упорядочены, поэтому числовые сравнения и проверки на числовое равенство принимают значение false, если хотя бы один или оба операнда являются NaN. В частности, проверка числового равенства значения самому себе принимает значение false тогда и только тогда, когда значение равно NaN. Проверка числового неравенства принимает значение true, если один из операндов равен NaN.
Тип returnAddress используется инструкциями виртуальной машины Java jsr, ret и jsr_w (§jsr, §ret, §jsr_w). Значения типа returnAddress — указатели на инструкции виртуальной машины Java. В отличие от числовых примитивных типов, тип returnAddress не соответствует какому-либо типу языка программирования Java и не может быть изменён работающей программой.
Хотя виртуальная машина Java определяет тип boolean, она предоставляет лишь очень ограниченную поддержку для него. Нет инструкций виртуальной машины Java, специально предназначенных для операций с значениями boolean. Вместо этого выражения в языке программирования Java, работающие со значениями boolean, компилируются с использованием значений типа данных int виртуальной машины Java.
Виртуальная машина Java напрямую поддерживает массивы boolean. Инструкция newarray (§newarray) позволяет создавать массивы boolean. Элементы массива типа boolean обращаются и изменяются с помощью инструкций для массивов byte baload и bastore (§baload, §bastore).
В реализации виртуальной машины Java от Oracle, массивы boolean в языке программирования Java кодируются как массивы byte виртуальной машины Java, используя 8 бит на каждый элемент boolean.
Виртуальная машина Java кодирует компоненты массива boolean, используя 1 для представления true и 0 для представления false. Там, где компиляторы языка программирования Java преобразуют значения boolean в значения типа int виртуальной машины Java, компиляторы должны использовать такое же кодирование.
Существуют три типа reference: типы классов, типы массивов и типы интерфейсов. Их значения являются ссылками на динамически созданные экземпляры классов, массивов или экземпляры классов или массивов, реализующих интерфейсы, соответственно.
Тип массива состоит из типа компонента с единственной размерностью (длина которой не задаётся типом). Тип компонента массива может сам быть типом массива. Если, начиная с любого типа массива, мы рассматриваем его тип компонента, а затем (если это также тип массива) тип компонента этого типа и так далее, то в конечном итоге мы должны достигнуть типа компонента, который не является типом массива; это называется типом элемента типа массива. Тип элемента типа массива обязательно является примитивным типом, типом класса или типом интерфейса.
Значение reference может также быть специальной нулевой ссылкой, ссылкой на ни один объект, которая здесь обозначается как null. Ссылка null изначально не имеет типа времени выполнения, но может быть приведена к любому типу. Значение по умолчанию для типа reference - null.
Спецификация виртуальной машины Java не предписывает конкретное кодирование значения null.
Java Virtual Machine определяет различные области данных во время выполнения, которые используются во время выполнения программы. Некоторые из этих областей данных создаются при запуске Java Virtual Machine и уничтожаются только при выходе из Java Virtual Machine. Другие области данных являются потоковыми. Области данных, относящиеся к потоку, создаются при создании потока и уничтожаются при выходе из потока.
Java Virtual Machine может поддерживать множество потоков выполнения одновременно (JLS §17). Каждый поток Java Virtual Machine имеет свой собственный регистр pc (счетчик программ). В любой момент времени каждый поток Java Virtual Machine выполняет код одного метода, а именно текущего метода (§2.6) для этого потока. Если этот метод не native, регистр pc содержит адрес инструкции Java Virtual Machine, которая в данный момент выполняется. Если метод, который в данный момент выполняется потоком, native, значение регистра pc Java Virtual Machine не определено. Регистр pc Java Virtual Machine достаточно широк, чтобы вместить returnAddress или нативный указатель на конкретной платформе.
Каждый поток Java Virtual Machine имеет частный стек Java Virtual Machine, созданный одновременно с потоком. Стек Java Virtual Machine хранит фреймы (§2.6). Стек Java Virtual Machine аналогичен стеку обычного языка, такого как C: он содержит локальные переменные и частичные результаты, а также играет роль в вызове и возврате методов. Поскольку стек Java Virtual Machine никогда не манипулируется напрямую, за исключением добавления и удаления фреймов, фреймы могут быть выделены в куче. Память для стека Java Virtual Machine не обязательно должна быть непрерывной.
В The Java Virtual Machine Specification, First Edition, стек Java Virtual Machine был известен как Java stack.
Эта спецификация разрешает стекам Java Virtual Machine либо иметь фиксированный размер, либо динамически расширяться и сжиматься по мере необходимости вычислений. Если стеки Java Virtual Machine имеют фиксированный размер, размер каждого стека Java Virtual Machine может быть выбран независимо при создании этого стека.
Реализация Java Virtual Machine может предоставить программисту или пользователю управление начальным размером стеков Java Virtual Machine, а также, в случае динамически расширяющихся или сжимающихся стеков Java Virtual Machine, управление максимальным и минимальным размерами.
Со следующими исключительными условиями связаны стеки Java Virtual Machine:
-
Если вычисление в потоке требует стека Java Virtual Machine большего размера, чем разрешено, Java Virtual Machine выдает
StackOverflowError. -
Если стеки Java Virtual Machine могут быть динамически расширены, и предпринята попытка расширения, но недостаточно памяти для выполнения расширения, или если недостаточно памяти для создания начального стека Java Virtual Machine для нового потока, Java Virtual Machine выдает
OutOfMemoryError.
Java Virtual Machine имеет heap, который совместно используется всеми потоками Java Virtual Machine. Heap — это область данных во время выполнения, из которой выделяется память для всех экземпляров классов и массивов.
Heap создается при запуске виртуальной машины. Память Heap для объектов возвращается системой автоматического управления памятью (известной как сборщик мусора); объекты никогда не освобождаются явно. Java Virtual Machine не предполагает какой-либо конкретный тип системы автоматического управления памятью, и метод управления памятью может быть выбран в соответствии с системными требованиями разработчика. Heap может иметь фиксированный размер или может быть расширен по мере необходимости вычислений и может быть сжат, если больший Heap становится ненужным. Память для Heap не обязательно должна быть непрерывной.
Реализация Java Virtual Machine может предоставить программисту или пользователю управление начальным размером Heap, а также, если Heap может быть динамически расширен или сжат, управление максимальным и минимальным размером Heap.
Со следующим исключительным условием связан Heap:
-
Если вычисление требует больше Heap, чем может быть предоставлено системой автоматического управления памятью, Java Virtual Machine выдает
OutOfMemoryError.
Java Virtual Machine имеет method area, который совместно используется всеми потоками Java Virtual Machine. Method Area аналогична области хранения для скомпилированного кода обычного языка или аналогична сегменту «text» в процессе операционной системы. Она хранит структуры на класс, такие как пул констант во время выполнения, данные полей и методов, а также код для методов и конструкторов, включая специальные методы (§2.9), используемые при инициализации классов и экземпляров и инициализации интерфейсов.
Method Area создается при запуске виртуальной машины. Хотя Method Area логически является частью Heap, простые реализации могут не выбирать сборку мусора или сжатие. Эта версия спецификации Java Virtual Machine не предписывает местоположение Method Area или политики, используемые для управления скомпилированным кодом. Method Area может иметь фиксированный размер или может быть расширена по мере необходимости вычислений и может быть сжата, если больший Method Area становится ненужным. Память для Method Area не обязательно должна быть непрерывной.
Реализация Java Virtual Machine может предоставить программисту или пользователю управление начальным размером Method Area, а также, в случае Method Area переменного размера, управление максимальным и минимальным размером Method Area.
Со следующим исключительным условием связана Method Area:
-
Если память в Method Area не может быть предоставлена для удовлетворения запроса на выделение памяти, Java Virtual Machine выдает
OutOfMemoryError.
Пул констант во время выполнения — это представление таблицы constant_pool в файле class (§4.4) для каждого класса или интерфейса во время выполнения. Он содержит несколько видов констант, от числовых литералов, известных во время компиляции, до ссылок на методы и поля, которые должны быть разрешены во время выполнения. Пул констант во время выполнения выполняет функцию, аналогичную таблице символов для обычного языка программирования, хотя он содержит более широкий диапазон данных, чем типичная таблица символов.
Каждый пул констант во время выполнения выделяется из Method Area Java Virtual Machine (§2.5.4). Пул констант во время выполнения для класса или интерфейса строится при создании класса или интерфейса (§5.3) Java Virtual Machine.
Со следующим исключительным условием связано построение пула констант во время выполнения для класса или интерфейса:
-
При создании класса или интерфейса, если для построения пула констант во время выполнения требуется больше памяти, чем может быть предоставлено в Method Area Java Virtual Machine, Java Virtual Machine выдает
OutOfMemoryError.
См. §5 для получения информации о построении пула констант во время выполнения.
Реализация Java Virtual Machine может использовать обычные стеки, разговорно называемые "C-стеками", для поддержки native методов (методы, написанные на языке, отличном от языка программирования Java). Стеки нативных методов также могут использоваться реализацией интерпретатора набора инструкций Java Virtual Machine на языке, таком как C. Реализации Java Virtual Machine, которые не могут загрузить native методы и не полагаются сами на обычные стеки, не обязаны предоставлять стеки нативных методов. Если они предоставляются, стеки нативных методов обычно выделяются на каждый поток при создании каждого потока.
Этот спецификация допускает, что стеки нативных методов могут быть либо фиксированного размера, либо динамически расширяться и сжиматься по мере необходимости вычисления. Если стеки нативных методов имеют фиксированный размер, размер каждого стека нативных методов может выбираться независимо при создании этого стека.
Реализация Java Virtual Machine может предоставить программисту или пользователю контроль над начальным размером стеков нативных методов, а также, в случае стеков нативных методов переменного размера, контроль над максимальным и минимальным размерами стека методов.
Следующие исключительные ситуации связаны со стеками нативных методов:
-
Если вычисления в потоке требуют стека нативных методов большего размера, чем разрешено, Java Virtual Machine выбрасывает исключение
StackOverflowError. -
Если стеки нативных методов могут быть динамически расширены, а попытка расширения стека нативных методов, но недостаточно памяти может быть выделена, или если недостаточно памяти может быть выделено для создания начального стека нативных методов для нового потока, Java Virtual Machine выбрасывает исключение
OutOfMemoryError.
Фрейм используется для хранения данных и промежуточных результатов, а также для выполнения динамической связи, возврата значений для методов и обработки исключений.
Каждый раз при вызове метода создается новый фрейм. Фрейм уничтожается при завершении вызова его метода, будь то нормальное завершение или внезапное (выбрасывание необработанного исключения). Фреймы выделяются из стека виртуальной машины Java (§2.5.2) потока, создавшего фрейм. Каждый фрейм имеет свой массив локальных переменных (§2.6.1), свой операционный стек (§2.6.2) и ссылку на пул постоянных значений выполнения (§2.5.5) класса текущего метода.
Фрейм может быть расширен дополнительной информацией, специфичной для реализации, например, информацией об отладке.
Размеры массива локальных переменных и операционного стека определяются во время компиляции и предоставляются вместе с кодом метода, связанного с фреймом (§4.7.3). Таким образом, размер структуры данных фрейма зависит только от реализации виртуальной машины Java, и память для этих структур может быть выделена одновременно при вызове метода.
В любой момент в заданном потоке управления активен только один фрейм — фрейм для выполняемого метода. Этот фрейм называется текущим фреймом, а его метод — текущим методом. Класс, в котором определен текущий метод, является текущим классом. Операции с локальными переменными и операционным стеком обычно выполняются по отношению к текущему фрейму.
Фрейм перестаёт быть текущим, если его метод вызывает другой метод или если его метод завершается. Когда метод вызывается, создаётся новый фрейм, который становится текущим при передаче управления новому методу. При возврате из метода текущий фрейм возвращает результат вызова его метода, если таковой имеется, предыдущему фрейму. Затем текущий фрейм удаляется, и предыдущий фрейм становится текущим.
Обратите внимание, что фрейм, созданный потоком, является локальным для этого потока и не может быть получен никаким другим потоком.
Каждый фрейм (§2.6) содержит массив переменных, известный как его локальные переменные. Длина массива локальных переменных фрейма определяется во время компиляции и предоставляется в двоичном представлении класса или интерфейса вместе с кодом метода, связанного с фреймом (§4.7.3).
Одна локальная переменная может содержать значение типа boolean, byte, char, short, int, float, reference или returnAddress. Пара локальных переменных может содержать значение типа long или double.
Локальные переменные адресуются по индексу. Индекс первой локальной переменной равен нулю. Целое число рассматривается как индекс в массиве локальных переменных только в том случае, если это целое число находится между нулём и размером массива локальных переменных минус единица.
Значение типа long или типа double занимает две последовательные локальные переменные. Такое значение может быть адресовано только с использованием меньшего индекса. Например, значение типа double, хранящееся в массиве локальных переменных по индексу n, фактически занимает локальные переменные с индексами n и n+1; однако, локальную переменную по индексу n+1 нельзя загружать. Она может быть записана. Однако при этом аннулируется содержимое локальной переменной n.
Виртуальная машина Java не требует, чтобы n было чётным. Проще говоря, значения типов long и double не обязаны быть выровнены по 64 битам в массиве локальных переменных. Реализаторы могут самостоятельно выбрать подходящий способ представления таких значений, используя две локальные переменные, зарезервированные для значения.
Виртуальная машина Java использует локальные переменные для передачи параметров при вызове метода. При вызове метода класса любые параметры передаются в последовательные локальные переменные, начиная с локальной переменной 0. При вызове метода экземпляра локальная переменная 0 всегда используется для передачи ссылки на объект, к которому применяется метод экземпляра (this на языке программирования Java). Любые параметры затем передаются в последовательные локальные переменные, начиная с локальной переменной 1.
Каждый фрейм (§2.6) содержит стек, работающий по принципу «последним вошел — первым вышел» (LIFO), известный как его операторный стек. Максимальная глубина операционного стека фрейма определяется во время компиляции и предоставляется вместе с кодом метода, связанного с фреймом (§4.7.3).
Когда контекст понятен, мы иногда будем называть операционный стек текущего фрейма просто операционным стеком.
Операторный стек пуст, когда фрейм, который его содержит, создан. Виртуальная машина Java предоставляет инструкции для загрузки констант или значений из локальных переменных или полей в операционный стек. Другие инструкции виртуальной машины Java берут операнды из операционного стека, выполняют операции над ними и помещают результат обратно в операционный стек. Операторный стек также используется для подготовки параметров для передачи методам и получения результатов методов.
Например, инструкция iadd (§iadd) складывает два значения int. Она требует, чтобы два значения int, которые нужно сложить, были двумя верхними значениями операционного стека, помещёнными туда предыдущими инструкциями. Оба значения int извлекаются из операционного стека. Они складываются, и их сумма помещается обратно в операционный стек. Подвычисления могут быть вложены в операционном стеке, что приводит к значениям, которые могут использоваться в охватывающем вычислении.
Каждый элемент операционного стека может содержать значение любого типа виртуальной машины Java, включая значение типа long или типа double.
Значения из операционного стека должны обрабатываться соответствующим их типам образом. Например, нельзя поместить два значения int и затем обрабатывать их как long, или поместить два значения float и затем складывать их с инструкцией iadd. Небольшое количество инструкций виртуальной машины Java (инструкции dup (§dup) и swap (§swap)) оперируют областями данных во время выполнения как сырыми значениями, не учитывая их конкретные типы; эти инструкции определены таким образом, что они не могут использоваться для изменения или разделения отдельных значений. Эти ограничения на манипуляции операционным стеком контролируются проверкой файлов class (§4.10).
В любой момент времени у операционного стека есть связанная глубина, где значение типа long или double вносит два единицы в глубину, а значение любого другого типа вносит одну единицу.
Каждый фрейм (§2.6) содержит ссылку на пул постоянных значений выполнения (§2.5.5) для типа текущего метода для поддержки динамической ссылки на код метода. Код метода файла class ссылается на вызываемые методы и переменные через символические ссылки. Динамическая ссылка преобразует эти символические ссылки на метод в конкретные ссылки на метод, загружая классы по мере необходимости для разрешения пока ещё неопределённых символов и преобразует обращения к переменным в соответствующие смещения в структурах хранения, связанных с местоположением этих переменных во время выполнения.
Эта поздняя привязка методов и переменных делает менее вероятным, что изменения в других классах, которые использует метод, повредят этот код.
Вызов метода завершается нормально, если этот вызов не вызывает исключение (§2.10), которое выбрасывается либо напрямую из виртуальной машины Java, либо в результате выполнения явного throw оператора. Если вызов текущего метода завершается нормально, то вызываемому методу может быть возвращено значение. Это происходит, когда вызываемый метод выполняет одну из инструкций возврата (§2.11.8), выбор которой должен быть соответствующим типу возвращаемого значения (если оно есть).
В этом случае текущий фрейм (§2.6) используется для восстановления состояния вызывающего метода, включая его локальные переменные и стек операндов, при этом счётчик команд вызывающего метода увеличивается соответствующим образом, чтобы перейти за инструкцию вызова метода. Затем выполнение продолжается нормально в фрейме вызывающего метода со возвращаемым значением (если оно есть), помещённым в стек операндов этого фрейма.
Вызов метода завершается с прерыванием, если выполнение инструкции виртуальной машины Java внутри метода вызывает бросок исключения (§2.10) виртуальной машиной Java, и это исключение не обрабатывается внутри метода. Выполнение инструкции athrow (§athrow) также вызывает явное исключение, и если исключение не перехватывается текущим методом, то происходит прерывание вызова метода. Вызов метода, который завершается с прерыванием, никогда не возвращает значение вызывающему методу.
Виртуальная машина Java не накладывает никаких конкретных ограничений на внутреннюю структуру объектов.
В некоторых реализациях виртуальной машины Java от Oracle ссылка на экземпляр класса является указателем на обработчик, который сам по себе является парой указателей: один на таблицу, содержащую методы объекта, и указатель на Class объект, представляющий тип объекта, а другой - на выделенную из кучи память для данных объекта.
Виртуальная машина Java включает подмножество арифметики с плавающей точкой, указанное в Стандарте IEEE для двоичной арифметики с плавающей точкой (ANSI/IEEE Std. 754-1985, Нью-Йорк).
Ключевые различия между арифметикой с плавающей точкой, поддерживаемой виртуальной машиной Java, и стандартом IEEE 754:
-
Операции с плавающей точкой в виртуальной машине Java не генерируют исключения, не перехватывают и не сигнализируют о исключительных ситуациях IEEE 754 (неправильная операция, деление на ноль, переполнение, недополнение или неточность). Виртуальная машина Java не имеет значения NaN, сигнализирующего о неточности.
-
Виртуальная машина Java не поддерживает сигнализирующие сравнения с плавающей точкой IEEE 754.
-
Округления в виртуальной машине Java всегда используют режим округления IEEE 754 до ближайшего значения. Неточные результаты округляются до ближайшего представимого значения, а при равенстве выбирается значение с нулевым наименее значимым битом. Это – стандартный режим IEEE 754. Но инструкции виртуальной машины Java, которые преобразуют значения типов с плавающей точкой в значения целочисленных типов, округляют в сторону нуля. Виртуальная машина Java не предоставляет средства изменения режима округления с плавающей точкой.
-
Виртуальная машина Java не поддерживает ни расширенный формат IEEE 754 с одинарной, ни с двойной точностью, за исключением случаев, когда значения двойной точности и значения с двойной расширенной экспонентой могут поддерживать формат с одинарной точностью. Множества значений с расширенной экспонентой с плавающей точкой одинарной и двойной точности, которые могут быть необязательно поддерживаемы, не соответствуют значениям расширенных форматов IEEE 754: расширенные форматы IEEE 754 требуют расширенной точности и расширенного диапазона экспоненты.
Каждый метод имеет режим с плавающей точкой, который является либо FP-строгим, либо не FP-строгим. Режим с плавающей точкой метода определяется значением флага ACC_STRICT элемента access_flags структуры method_info (§4.6), определяющего метод. Метод, для которого установлен этот флаг, является FP-строгим; в противном случае, метод не FP-строгий.
Обратите внимание, что эта связь флага ACC_STRICT подразумевает, что методы в классах, скомпилированных компилятором JDK версии 1.1 или более ранней, фактически не являются FP-строгими.
Мы будем говорить, что стек операндов имеет данный режим с плавающей точкой, когда метод, вызов которого создал фрейм, содержащий стек операндов, имеет этот режим с плавающей точкой. Аналогично, мы будем говорить, что инструкция виртуальной машины Java имеет данный режим с плавающей точкой, когда метод, содержащий эту инструкцию, имеет этот режим.
Если поддерживается множество значений с плавающей точкой с расширенной экспонентой (§2.3.2), значения типа float в стеке операндов, который не является FP-строгим, могут охватывать это множество значений, за исключением случаев, запрещённых преобразованием множеств значений (§2.8.3). Если поддерживается множество значений с двойной расширенной экспонентой (§2.3.2), значения типа double в стеке операндов, который не является FP-строгим, могут охватывать это множество значений, за исключением случаев, запрещённых преобразованием множеств значений.
Во всех других контекстах, будь то в стеке операндов или где-либо ещё, и независимо от режима с плавающей точкой, значения с плавающей точкой типов float и double могут охватывать только множество значений типа float и double соответственно. В частности, поля классов и экземпляров, элементы массивов, локальные переменные и параметры методов могут содержать только значения, взятые из стандартных множеств значений.
Реализация виртуальной машины Java, поддерживающая расширенное множество значений с плавающей точкой, может или должна, в определённых обстоятельствах, отображать значение связанного типа с плавающей точкой между расширенным и стандартным множествами значений. Такое преобразование множества значений не является преобразованием типа, а отображением между множествами значений, связанных с одним типом.
В указанных ситуациях преобразования множества значений, реализация может выполнить одну из следующих операций над значением:
-
Если значение типа
floatи не является элементом множества значений float, оно отображается на ближайший элемент множества значений float. -
Если значение типа
doubleи не является элементом множества значений double, оно отображается на ближайший элемент множества значений double.
Кроме того, в указанных ситуациях преобразования множества значений требуются следующие операции:
-
Предположим, выполнение инструкции виртуальной машины Java, которая не является FP-строгой, вызывает помещение значения типа
floatв стек операндов, который является FP-строгим, передачу в качестве параметра или сохранение в локальную переменную, поле или элемент массива. Если значение не является элементом множества значений float, оно отображается на ближайший элемент множества значений float. -
Предположим, выполнение инструкции виртуальной машины Java, которая не является FP-строгой, вызывает помещение значения типа
doubleв стек операндов, который является FP-строгим, передачу в качестве параметра или сохранение в локальную переменную, поле или элемент массива. Если значение не является элементом множества значений double, оно отображается на ближайший элемент множества значений double.
Такие требуемые преобразования множеств значений могут произойти в результате передачи параметра типа с плавающей точкой во время вызова метода, включая вызов метода native; возвращения значения типа с плавающей точкой из метода, который не является FP-строгим, в метод, который является FP-строгим; или сохранения значения типа с плавающей точкой в локальную переменную, поле или массив в методе, который не является FP-строгим.
Не все значения из множества значений с расширенной экспонентой могут быть точно отображены в соответствующее значение в стандартном множестве значений. Если значение, которое отображается, слишком велико для точного представления (его экспонента больше, чем разрешено стандартным множеством значений), оно преобразуется в бесконечность (положительную или отрицательную) соответствующего типа. Если значение, которое отображается, слишком мало для точного представления (его экспонента меньше, чем разрешено стандартным множеством значений), оно округляется до ближайшего значения денормализованного значения или нуля того же знака.
Преобразование множеств значений сохраняет бесконечности и NaN и не может изменить знак преобразуемого значения. Преобразование множеств значений не влияет на значение, которое не является типом с плавающей точкой.
На уровне виртуальной машины Java, каждый конструктор, написанный на языке программирования Java (JLS §8.8), представлен как метод инициализации экземпляра, имеющий специальное имя <init>. Это имя предоставляется компилятором. Поскольку имя <init> не является допустимым идентификатором, его нельзя использовать напрямую в программе, написанной на языке программирования Java. Методы инициализации экземпляров могут вызываться только внутри виртуальной машины Java инструкцией invokespecial (§invokespecial), и только для неинициализированных экземпляров класса. Метод инициализации экземпляра наследует права доступа (JLS §6.6) от конструктора, из которого он был получен.
Класс или интерфейс имеют не более одного метода инициализации класса или интерфейса и инициализируется (§5.5) вызовом этого метода. Метод инициализации класса или интерфейса имеет специальное имя <clinit>, не принимает аргументов и имеет тип void (§4.3.3).
Другие методы с именем <clinit> в файле class не имеют значения. Они не являются методами инициализации класса или интерфейса. Их нельзя вызвать ни одной инструкцией виртуальной машины Java, и виртуальная машина Java их никогда не вызывает.
В файле class, версия которого 51.0 или выше, метод должен дополнительно иметь свой флаг ACC_STATIC (§4.6), чтобы быть методом инициализации класса или интерфейса.
Это требование появилось в Java SE 7. В файле класса, версия которого 50.0 или ниже, метод с именем <clinit>, который имеет тип void и не принимает аргументов, считается методом инициализации класса или интерфейса независимо от установки флага ACC_STATIC.
Имя <clinit> предоставляется компилятором. Поскольку имя <clinit> не является допустимым идентификатором, его нельзя использовать напрямую в программе, написанной на языке программирования Java. Методы инициализации класса и интерфейса вызываются виртуальной машиной Java неявно; они никогда не вызываются напрямую из какой-либо инструкции виртуальной машины Java, а вызываются только косвенно как часть процесса инициализации класса.
Метод является полиморфным по сигнатуре, если и только если выполняются все следующие условия:
-
Он объявлен в классе
java.lang.invoke.MethodHandle. -
Он имеет один формальный параметр типа
Object[]. -
Он имеет тип возвращаемого значения
Object. -
У него установлены флаги
ACC_VARARGSиACC_NATIVE.
В Java SE 7 единственными полиморфными по сигнатуре методами являются методы invoke и invokeExact класса java.lang.invoke.MethodHandle.
Виртуальная машина Java предоставляет специальное обращение к методам, полиморфным по сигнатуре, в инструкции invokevirtual (§invokevirtual), чтобы осуществить вызов обработчика метода. Обработчик метода — это типизированная, непосредственно исполняемая ссылка на базовый метод, конструктор, поле или подобную низкоуровневую операцию (§5.4.3.5), с необязательными преобразованиями аргументов или значений возврата. Эти преобразования довольно общие и включают такие шаблоны, как преобразование, вставка, удаление и подстановка. Дополнительную информацию можно найти в пакете java.lang.invoke в API платформы Java SE.
Исключение в виртуальной машине Java представляется экземпляром класса Throwable или одного из его подклассов. Бросание исключения приводит к немедленному нелокальному переводу управления из точки, в которой было брошено исключение.
Большинство исключений возникают синхронно в результате действия потока, в котором они возникают. В отличие от этого, асинхронное исключение может потенциально возникнуть в любой точке выполнения программы. Виртуальная машина Java бросает исключение по одной из трёх причин:
-
Была выполнена инструкция athrow (§athrow).
-
Виртуальной машиной Java было синхронно обнаружено аномальное условие выполнения. Эти исключения не выбрасываются в произвольной точке программы, а только синхронно после выполнения инструкции, которая либо:
-
Указывает на исключение как на возможный результат, например:
-
Когда инструкция воплощает операцию, которая нарушает семантику языка программирования Java, например, индексирование за пределами границ массива.
-
Когда возникает ошибка при загрузке или связывании части программы.
-
-
Приводит к превышению какого-либо ограничения на ресурс, например, при использовании слишком большого объёма памяти.
-
-
Произошло асинхронное исключение, потому что:
-
был вызван метод
stopклассаThreadилиThreadGroup, или -
возникла внутренняя ошибка в реализации виртуальной машины Java.
Методы
stopмогут вызываться одним потоком для воздействия на другой поток или на все потоки в заданной группе потоков. Они асинхронны, потому что могут возникнуть в любой точке выполнения другого потока или потоков. Внутренняя ошибка считается асинхронной (§6.3). -
Виртуальная машина Java может разрешить небольшое, но ограниченное количество выполнения перед выбросом асинхронного исключения. Эта задержка разрешается для того, чтобы оптимизированный код мог обнаруживать и выбрасывать эти исключения в точках, где практически возможно их обработка, соблюдая семантику языка программирования Java.
Простая реализация могла бы опрашивать асинхронные исключения в точке каждой инструкции передачи управления. Поскольку программа имеет конечный размер, это предоставляет ограничение на общую задержку при обнаружении асинхронного исключения. Поскольку асинхронное исключение не произойдёт между передачами управления, код генератора имеет некоторую гибкость в переупорядочении вычислений между передачами управления для повышения производительности. Рекомендуется дополнительное чтение статьи Polling Efficiently on Stock Hardware Марка Феле, Proc. 1993 Conference on Functional Programming and Computer Architecture, Копенгаген, Дания, стр. 179–187.
Исключения, брошенные виртуальной машиной Java, точные: когда происходит передача управления, все эффекты инструкций, выполненных до точки, из которой выбрасывается исключение, должны казаться выполненными. Никакие инструкции, которые появляются после точки, из которой выбрасывается исключение, не могут казаться оценёнными. Если оптимизированный код спекулятивно выполнил некоторые инструкции, следующие за точкой, в которой возникает исключение, такой код должен быть готов скрыть это спекулятивное выполнение от видимого пользователю состояния программы.
Каждый метод в виртуальной машине Java может быть связан с нулём или более обработчиками исключений. Обработчик исключений определяет диапазон смещений в коде виртуальной машины Java, реализующем метод, для которого обработчик исключений активен, описывает тип исключения, которое обработчик исключений может обработать, и указывает местоположение кода, который должен обрабатывать это исключение. Исключение соответствует обработчику исключений, если смещение инструкции, вызвавшей исключение, находится в диапазоне смещений обработчика исключений, и тип исключения является тем же классом, что и класс исключения, которое обработчик исключений обрабатывает, или подклассом этого класса. При возникновении исключения виртуальная машина Java ищет соответствующий обработчик исключений в текущем методе. Если такой обработчик исключений найден, система переходит к коду обработки исключений, указанному соответствующим обработчиком.
Если такой обработчик исключений не найден в текущем методе, вызов текущего метода завершается внезапно (§2.6.5). При внезапном завершении стек операндов и локальные переменные текущего вызова метода удаляются, а его кадр удаляется, восстанавливая кадр вызывающего метода. Затем исключение повторно бросается в контексте кадра вызывающей функции и так далее, продолжая по цепочке вызова методов. Если подходящий обработчик исключений не найден до достижения вершины цепочки вызова методов, выполнение потока, в котором возникло исключение, завершается.
Порядок, в котором ищутся обработчики исключений метода, важен. Внутри файла class обработчики исключений каждого метода хранятся в таблице (§4.7.3). При выполнении, когда возникает исключение, виртуальная машина Java ищет обработчики исключений текущего метода в порядке, в котором они появляются в соответствующей таблице обработчиков исключений в файле class, начиная с начала этой таблицы.
Обратите внимание, что виртуальная машина Java не навязывает вложенность или какой-либо порядок записей таблицы исключений метода. Семантика обработки исключений языка программирования Java реализуется только посредством сотрудничества с компилятором (§3.12). Когда файлы class генерируются другими средствами, определённая процедура поиска гарантирует, что все реализации виртуальной машины Java будут вести себя согласованно.
Инструкция виртуальной машины Java состоит из одного байта кода операции, указывающего операцию, которую нужно выполнить, за которой следуют ноль или более операндов, предоставляющих аргументы или данные, используемые операцией. Многие инструкции не имеют операндов и состоят только из кода операции.
Игнорируя исключения, внутренний цикл интерпретатора виртуальной машины Java эффективно
do {
atomically calculate pc and fetch opcode at pc;
if (operands) fetch operands;
execute the action for the opcode;
} while (there is more to do);
Количество и размер операндов определяются кодом операции. Если операнд имеет размер более одного байта, то он хранится в порядке big-endian - старший байт первым. Например, 16-битный беззнаковый индекс в локальные переменные хранится как два беззнаковых байта, byte1 и byte2, так что его значение равно (byte1 << 8) | byte2.
Поток байткода инструкций выровнен только по одному байту. Два исключения составляют инструкции lookupswitch и tableswitch (§lookupswitch, §tableswitch), которые дополняются для принудительного внутреннего выравнивания некоторых своих операндов на 4-байтовых границах.
Решение ограничить код операции виртуальной машины Java одним байтом и отказаться от выравнивания данных внутри скомпилированного кода отражает осознанный уклон в пользу компактности, возможно, в ущерб производительности в наивных реализациях. Однобайтовый код операции также ограничивает размер набора инструкций. Отсутствие предположения о выравнивании данных означает, что немедленные данные размером больше байта должны быть построены из байтов во время выполнения на многих машинах.
Большинство инструкций в наборе инструкций виртуальной машины Java кодируют информацию о типах операций, которые они выполняют. Например, инструкция iload (§iload) загружает содержимое локальной переменной, которая должна быть int, в стек операндов. Инструкция fload (§fload) делает то же самое со значением float. Эти две инструкции могут иметь идентичные реализации, но имеют разные коды операций.
Для большинства типизированных инструкций тип инструкции представлен явно в мнемонике кода операции буквой: i для int операции, l для long, s для short, b для byte, c для char, f для float, d для double и a для reference. Некоторые инструкции, тип которых однозначен, не имеют буквы типа в своей мнемонике. Например, arraylength всегда работает с объектом, который является массивом. Некоторые инструкции, такие как goto, безусловный переход управления, не работают с типизированными операндами.
Учитывая размер кода операции виртуальной машины Java в один байт, кодирование типов в кодах операций создаёт давление на проектирование набора инструкций. Если каждая типизированная инструкция поддерживала все типы данных виртуальной машины Java во время выполнения, инструкций было бы больше, чем можно было бы представить в одном байте. Вместо этого набор инструкций виртуальной машины Java предоставляет пониженный уровень поддержки типов для определённых операций. Другими словами, набор инструкций намеренно не ортогонален. Для преобразования между неподдерживаемыми и поддерживаемыми типами данных, при необходимости, могут использоваться отдельные инструкции.
Таблица 2.2 обобщает поддержку типов в наборе инструкций виртуальной машины Java. Конкретная инструкция с информацией о типе создаётся путём замены T в шаблоне инструкции в столбце кода операции буквой в столбце типа. Если столбец типа для некоторого шаблона инструкции и типа пуст, то не существует инструкции, поддерживающей этот тип операции. Например, существует инструкция загрузки для типа int, iload, но нет инструкции загрузки для типа byte.
Обратите внимание, что большинство инструкций в таблице 2.2 не имеют форм для целочисленных типов byte, char и short. Ни одна из них не имеет форм для типа boolean. Компилятор кодирует загрузки литерных значений типов byte и short с использованием инструкций виртуальной машины Java, которые расширяют эти значения до значений типа int во время компиляции или выполнения. Загрузки литерных значений типов boolean и char кодируются с использованием инструкций, которые расширяют литерные значения до значения типа int во время компиляции или выполнения. Аналогично, загрузки из массивов значений типа boolean, byte, short и char кодируются с использованием инструкций виртуальной машины Java, которые расширяют значения со знаком или без знака до значений типа int. Таким образом, большинство операций над значениями фактических типов boolean, byte, char и short правильно выполняются инструкциями, работающими со значениями вычислительного типа int.
Таблица 2.2. Поддержка типов в наборе команд виртуальной машины 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.3.
Некоторые инструкции виртуальной машины Java, такие как pop и swap, работают со стеком операндов без учета типа; однако такие инструкции ограничены использованием только значений определенных категорий вычислительных типов, также указанных в таблице 2.3.
Таблица 2.3. Фактические и вычислительные типы в виртуальной машине Java
| Фактический тип | Вычислительный тип | Категория |
|---|---|---|
boolean | int | 1 |
byte | int | 1 |
char | int | 1 |
short | int | 1 |
int | int | 1 |
float | float | 1 |
reference | reference | 1 |
returnAddress | returnAddress | 1 |
long | long | 2 |
double | double | 2 |
Инструкции загрузки и сохранения передают значения между локальными переменными (§2.6.1) и стеком операндов (§2.6.2) кадра виртуальной машины Java (§2.6):
-
Загрузка локальной переменной в стек операндов: iload, iload_<n>, lload, lload_<n>, fload, fload_<n>, dload, dload_<n>, aload, aload_<n>.
-
Сохранение значения из стека операндов в локальную переменную: istore, istore_<n>, lstore, lstore_<n>, fstore, fstore_<n>, dstore, dstore_<n>, astore, astore_<n>.
-
Загрузка константы в стек операндов: bipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_m1, iconst_<i>, lconst_<l>, fconst_<f>, dconst_<d>.
-
Доступ к большему количеству локальных переменных с использованием более широкого индекса или к большему непосредственному операнду: wide.
Инструкции, которые обращаются к полям объектов и элементам массивов (§2.11.5), также передают данные в стек операндов и из него.
Мнемоники инструкций, показанные выше с заключенными в угловые скобки буквами (например, iload_<n>), обозначают семейства инструкций (с членами iload_0, iload_1, iload_2 и iload_3 в случае iload_<n>). Такие семейства инструкций являются специализациями дополнительной универсальной инструкции (iload), которая принимает один операнд. Для специализированных инструкций операнд неявен и не требует хранения или извлечения. Семантика в остальном одинакова (iload_0 означает то же самое, что и iload с операндом 0). Буква между угловыми скобками указывает тип неявного операнда для этого семейства инструкций: для <n> — неотрицательное целое число; для <i> — int; для <l> — long; для <f> — float; и для <d> — double. Формы для типа int используются во многих случаях для выполнения операций над значениями типа byte, char и short (§2.11.1).
Эта нотация для семейств инструкций используется на протяжении всего данного спецификационного документа.
Арифметические инструкции вычисляют результат, который обычно является функцией двух значений в стеке операндов, помещая результат обратно в стек операндов. Существует два основных вида арифметических инструкций: те, которые работают со значениями целых чисел, и те, которые работают с плавающей точкой. В каждом из этих видов арифметические инструкции специализируются на числовых типах виртуальной машины Java. Нет прямой поддержки целочисленной арифметики для значений типов byte, short и char (§2.11.1), или для значений типа boolean; эти операции обрабатываются инструкциями, работающими с типом int. Целочисленные и инструкции с плавающей точкой также отличаются по своему поведению при переполнении и делении на ноль. Арифметические инструкции следующие:
-
Сложение: iadd, ladd, fadd, dadd.
-
Вычитание: isub, lsub, fsub, dsub.
-
Умножение: imul, lmul, fmul, dmul.
-
Деление: idiv, ldiv, fdiv, ddiv.
-
Остаток от деления: irem, lrem, frem, drem.
-
Отрицание: ineg, lneg, fneg, dneg.
-
Сдвиг: ishl, ishr, iushr, lshl, lshr, lushr.
-
Битовое ИЛИ: ior, lor.
-
Битовое И: iand, land.
-
Битовое исключающее ИЛИ: ixor, lxor.
-
Инкремент локальной переменной: iinc.
-
Сравнение: dcmpg, dcmpl, fcmpg, fcmpl, lcmp.
Семантика операторов языка программирования Java над целыми и значениями с плавающей точкой (JLS §4.2.2, JLS §4.2.4) напрямую поддерживается семантикой набора инструкций виртуальной машины Java.
Виртуальная машина Java не указывает переполнение во время операций с целочисленными типами данных. Единственными целочисленными операциями, которые могут вызвать исключение, являются целочисленные инструкции деления (idiv и ldiv) и целочисленные инструкции остатка от деления (irem и lrem), которые вызывают исключение ArithmeticException, если делитель равен нулю.
Операции виртуальной машины Java с числами с плавающей точкой ведут себя в соответствии со спецификацией IEEE 754. В частности, виртуальная машина Java требует полной поддержки long чисел с плавающей точкой IEEE 754 и int, что упрощает доказательство желаемых свойств конкретных численных алгоритмов.
Виртуальная машина Java требует, чтобы арифметические операции с плавающей точкой вели себя так, как если бы каждый оператор с плавающей точкой округлял свой результат с плавающей точкой до точности результата. long результаты должны быть округлены до представимого значения, ближайшего к бесконечно точному результату; если два ближайших представимых значения находятся на одинаковом расстоянии, выбирается то, у которого наименее значимый бит равен нулю. Это стандартный режим округления IEEE 754, известный как режим округления до ближайшего значения.
Виртуальная машина Java использует режим IEEE 754 float при преобразовании значения с плавающей точкой в целое число. Это приводит к усечению числа; любые биты значащей части, которые представляют дробную часть значения операнда, отбрасываются. Режим округления к нулю выбирает в качестве своего результата значение типа, наиболее близкое к, но не больше по величине, чем бесконечно точный результат.
Операторы с плавающей точкой виртуальной машины 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, которые являются FP-строгими (§2.8.2), также сохраняют числовое значение точно; однако такие преобразования, которые не являются FP-строгими, могут потерять информацию о величине преобразованного значения.
Преобразование значения типа int или long в float или значения типа long в double может привести к потере точности, то есть может привести к потере некоторых младших битов значения; полученное значение с плавающей точкой является правильно округленным значением целочисленного значения, используя режим округления IEEE 754 до ближайшего значения.
Расширяющее числовое преобразование значения int в long просто расширяет знаковое представление значения int со знаком в формате большего размера. Расширяющее числовое преобразование значения char в целочисленный тип нулевое расширение представления значения char для заполнения формата большего размера.
Несмотря на то, что потеря точности может произойти, расширяющие числовые преобразования никогда не заставят виртуальную машину Java выбросить исключение во время выполнения (не следует путать с исключением IEEE 754 с плавающей точкой).
Обратите внимание, что расширяющие числовые преобразования не существуют от целочисленных типов byte, char и short к типу int. Как отмечается в §2.11.1, значения типа byte, char и short неявно расширяются до типа int, что делает эти преобразования неявными.
Виртуальная машина Java также напрямую поддерживает следующие сужающие числовые преобразования:
-
intвbyte,shortилиchar -
longвint -
floatвintилиlong -
doubleвint,longилиfloat
Инструкции сужающих числовых преобразований — это i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l и d2f. Сужающее числовое преобразование может привести к значению с другим знаком, другой величиной или обоим; оно может тем самым потерять точность.
Сужающее числовое преобразование значения int или long в целочисленный тип T просто отбрасывает все, кроме N младших разрядов, где N — количество битов, используемых для представления типа T. Это может привести к тому, что полученное значение не будет иметь тот же знак, что и входное значение.
При сужающем числовом преобразовании значения с плавающей точкой в целочисленный тип T, где T — либо int, либо long, значение с плавающей точкой преобразуется следующим образом:
-
Если значение с плавающей точкой равно NaN, результатом преобразования является
intилиlong0. -
В противном случае, если значение с плавающей точкой не является бесконечностью, значение с плавающей точкой округляется до целочисленного значения V с использованием режима округления IEEE 754 к нулю. Есть два случая:
-
Если T —
longи это целое значение может быть представлено какlong, то результатом являетсяlongзначение V. -
Если T типа
intи это целое значение может быть представлено какint, то результатом являетсяintзначение V.
-
-
В противном случае:
-
Значение должно быть слишком маленьким (отрицательное значение большой величины или отрицательная бесконечность), и результатом является наименьшее представимое значение типа
intилиlong. -
Или значение должно быть слишком большим (положительное значение большой величины или положительная бесконечность), и результатом является наибольшее представимое значение типа
intилиlong.
-
Сужающее числовое преобразование из double в float ведет себя в соответствии с IEEE 754. Результат правильно округляется с использованием режима округления IEEE 754 до ближайшего значения. Значение, слишком маленькое, чтобы быть представленным как float, преобразуется в положительный или отрицательный ноль типа float; значение, слишком большое, чтобы быть представленным как float, преобразуется в положительную или отрицательную бесконечность. double NaN всегда преобразуется в float NaN.
Несмотря на то, что переполнение, недополнение или потеря точности могут произойти, сужающие преобразования между числовыми типами никогда не заставят виртуальную машину Java выбросить исключение во время выполнения (не следует путать с исключением IEEE 754 с плавающей точкой).
Хотя классы экземпляров и массивы являются объектами, виртуальная машина Java создает и обрабатывает классы экземпляров и массивы с использованием отдельных наборов инструкций:
-
Создать новый экземпляр класса: new.
-
Создать новый массив: newarray, anewarray, multianewarray.
-
Доступ к полям классов (
staticполя, известные как переменные класса) и полям экземпляров классов (не-staticполя, известные как переменные экземпляров): getfield, putfield, getstatic, putstatic. -
Загрузить компонент массива в стек операндов: baload, caload, saload, iaload, laload, faload, daload, aaload.
-
Сохранить значение из стека операндов в качестве компонента массива: bastore, castore, sastore, iastore, lastore, fastore, dastore, aastore.
-
Получить длину массива: arraylength.
-
Проверить свойства экземпляров классов или массивов: instanceof, checkcast.
Для непосредственного управления стеком операндов предоставляется ряд команд: pop, pop2, dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2, swap.
Команды передачи управления условно или безусловно заставляют виртуальную машину Java продолжить выполнение с другой командой, отличной от следующей за командой передачи управления. Это:
-
Условный переход: ifeq, ifne, iflt, ifle, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_icmple, if_icmpgt if_icmpge, if_acmpeq, if_acmpne.
-
Составной условный переход: tableswitch, lookupswitch.
-
Безусловный переход: goto, goto_w, jsr, jsr_w, ret.
У виртуальной машины Java есть отдельные наборы команд, которые условно переходят по результатам сравнения данных типов int и reference. Она также имеет отдельные условные команды перехода, которые проверяют на null, и поэтому не требуется указывать конкретное значение для null (§2.4).
Условные переходы по результатам сравнения данных типов boolean, byte, char и short выполняются с использованием команд сравнения int (§2.11.1). Условный переход по результатам сравнения данных типов long, float или double инициируется командой, которая сравнивает данные и производит int результат сравнения (§2.11.3). Последующая команда сравнения int проверяет этот результат и выполняет условный переход. Благодаря акценту на int сравнениях, виртуальная машина Java предоставляет богатый набор инструкций условного перехода для типа int.
Все int условные команды передачи управления выполняют сравнения со знаком.
Следующие пять команд вызывают методы:
-
invokevirtual вызывает метод экземпляра объекта, диспатчинг по типу объекта (виртуальный). Это обычный диспатчинг методов в языке программирования Java.
-
invokeinterface вызывает метод интерфейса, ища методы, реализованные конкретным объектом во время выполнения, чтобы найти соответствующий метод.
-
invokespecial вызывает метод экземпляра, требующий специальной обработки, будь то метод инициализации экземпляра (§2.9), метод
privateили метод суперкласса. -
invokestatic вызывает метод класса (статический метод) в указанном классе.
-
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.