Spec-Zone.ru › Java Virtual Machine Specification 24

Глава 2. Структура виртуальной машины Java

Содержание

2.1. Формат файла class
2.2. Типы данных
2.3. Примитивные типы и значения
2.3.1. Целочисленные типы и значения
2.3.2. Типы и значения с плавающей точкой
2.3.3. Тип returnAddress и значения
2.3.4. Тип boolean
2.4. Ссылочные типы и значения
2.5. Области данных во время выполнения
2.5.1. Регистр pc
2.5.2. Стек виртуальной машины Java
2.5.3. Куча
2.5.4. Область методов
2.5.5. Пул постоянных во время выполнения
2.5.6. Стек методов нативных интерфейсов
2.6. Фреймы
2.6.1. Локальные переменные
2.6.2. Стек операндов
2.6.3. Динамическая ссылка
2.6.4. Окончание вызова метода по умолчанию
2.6.5. Прерывание вызова метода
2.7. Представление объектов
2.8. Арифметика с плавающей точкой
2.9. Специальные методы
2.9.1. Методы инициализации экземпляров
2.9.2. Методы инициализации классов
2.9.3. Полиморфные методы сигнатуры
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 (например, их перевод в машинный код) оставляются на усмотрение разработчика.

Все ссылки на Юникод в этой спецификации даются по отношению к Стандарту Юникод, версия 16.0.0, доступному по адресу https://www.unicode.org/.

2.1. Формат файла class

Компилированный код, который будет выполняться виртуальной машиной Java, представлен в формате двоичных данных, не зависящем от аппаратной платформы и операционной системы, обычно (но не обязательно) хранящимся в файле, известном как формат файла class. Формат файла class точно определяет представление класса или интерфейса, включая такие детали, как порядок байтов, которые могут быть приняты как должное в платформенно-зависимом формате файла объекта.

В главе 4 "Формат файла class" подробно описывается формат файла class.

2.2. Типы данных

Как и язык программирования Java, виртуальная машина Java работает с двумя типами: примитивные типы и справочные типы. Соответственно, существуют два вида значений, которые могут храниться в переменных, передаваться в качестве аргументов, возвращаться методами и обрабатываться: примитивные значения и справочные значения.

Виртуальная машина Java ожидает, что почти вся проверка типов выполняется до выполнения, обычно компилятором, и не должна выполняться самой виртуальной машиной. Значения примитивных типов не должны быть помечены или иным образом проверяемы для определения их типов во время выполнения или для отличия от значений ссылочных типов. Вместо этого набор инструкций виртуальной машины Java различает типы своих операндов, используя инструкции, предназначенные для работы со значениями определённых типов. Например, iadd, ladd, fadd и dadd — это инструкции виртуальной машины Java, которые складывают два числовых значения и генерируют числовой результат, но каждая специализируется на своём типе операнда: int, long, float и double соответственно. Подробный обзор поддержки типов в наборе инструкций виртуальной машины Java см. в §2.11.1.

Виртуальная машина Java содержит явную поддержку объектов. Объект — это либо экземпляр динамически выделенного класса, либо массив. Ссылка на объект считается имеющей тип виртуальной машины Java reference. Ссылки являются полиморфными: одна ссылка также может быть значением нескольких типов классов, интерфейсов или массивов. Значения типа reference можно рассматривать как указатели на объекты. Может существовать более одной ссылки на объект. Объекты всегда обрабатываются, передаются и проверяются с помощью значений типа reference.

2.3. Примитивные типы и значения

Примитивные типы данных, поддерживаемые виртуальной машиной 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.

В первом издании спецификации виртуальной машины The Java® Virtual Machine Specification тип boolean не рассматривался как тип виртуальной машины Java. Однако значения boolean имеют ограниченную поддержку в виртуальной машине Java. Во втором издании спецификации The Java® Virtual Machine Specification этот вопрос был уточнен, и boolean был определён как тип.

Значения типа returnAddress — указатели на коды инструкций виртуальной машины Java. Из примитивных типов только тип returnAddress не напрямую связан с типом языка программирования Java.

2.3.1. Целочисленные типы и значения

Значения целочисленных типов виртуальной машины 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 включительно

2.3.2. Типы и значения с плавающей запятой

Типы с плавающей запятой — это 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 — положительное целое число, меньшее 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) для 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.

2.3.3. Тип returnAddress и его значения

Тип returnAddress используется инструкциями виртуальной машины Java jsr, ret и jsr_w (§jsr, §ret, §jsr_w). Значения типа returnAddress — это указатели на коды операций инструкций виртуальной машины Java. В отличие от числовых примитивных типов, тип returnAddress не соответствует какому-либо типу языка программирования Java и не может быть изменён работающей программой.

2.3.4. Тип boolean

Хотя виртуальная машина 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, компиляторы должны использовать такое же кодирование.

2.4. Ссылочные типы и значения

Существует три вида reference типов: типы классов, типы массивов и типы интерфейсов. Их значения представляют собой ссылки на динамически созданные экземпляры классов, массивы или экземпляры классов или массивы, реализующие интерфейсы соответственно.

Тип массива состоит из типа компоненты с одним измерением (длина которого не задаётся типом). Тип компоненты массива может сам быть типом массива. Если, начиная с любого типа массива, рассматривать его тип компоненты, а затем (если это также тип массива) тип компоненты этого типа и так далее, то в конечном итоге необходимо достичь типа компоненты, который не является типом массива; это называется типом элемента типа массива. Тип элемента массива обязательно является либо примитивным типом, либо типом класса, либо типом интерфейса.

reference значение также может быть специальной ссылкой null, ссылкой на ни один объект, которая здесь обозначается как null. Ссылка null изначально не имеет типа во время выполнения, но может быть приведена к любому типу. Значение по умолчанию для типа reference равно null.

В данном спецификации не регламентируется конкретное кодирование значения null.

2.5. Области данных во время выполнения

Виртуальная машина Java определяет различные области данных во время выполнения, используемые во время выполнения программы. Некоторые из этих областей данных создаются при запуске виртуальной машины Java и уничтожаются только при завершении работы виртуальной машины Java. Другие области данных относятся к потокам. Области данных, относящиеся к потокам, создаются при создании потока и уничтожаются при завершении потока.

2.5.1. Регистр pc

Виртуальная машина Java может поддерживать сразу несколько потоков выполнения (JLS §17). Каждый поток виртуальной машины Java имеет свой собственный регистр pc (счётчик команд). В любой момент каждый поток виртуальной машины Java выполняет код одного метода, а именно текущего метода (§2.6) для этого потока. Если этот метод не native, регистр pc содержит адрес инструкции виртуальной машины, которая в настоящее время выполняется. Если метод, который в настоящее время выполняется потоком, является native, значение регистра pc виртуальной машины не определено. Регистр pc виртуальной машины достаточно широк, чтобы вместить returnAddress или указатель нативный на конкретной платформе.

2.5.2. Стек виртуальной машины Java

Каждый поток виртуальной машины 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.

2.5.3. Куча

Виртуальная машина Java имеет кучу, которая совместно используется всеми потоками виртуальной машины Java. Куча — это область данных во время выполнения, из которой выделяется память для всех экземпляров классов и массивов.

Куча создаётся при запуске виртуальной машины. Хранение объектов в куче восстанавливается системой автоматического управления памятью (известной как мусорный сборщик); объекты никогда не удаляются явно. Виртуальная машина Java не предполагает какой-либо конкретной системы автоматического управления памятью, и метод управления памятью может быть выбран в соответствии с системными требованиями реализатора. Куча может иметь фиксированный размер или может расширяться по мере необходимости для вычисления и может сжиматься, если больше кучи становится не нужно. Память для кучи не обязана быть непрерывной.

Реализация виртуальной машины Java может предоставить программисту или пользователю контроль над начальным размером кучи, а также, если куча может быть динамически расширена или сжата, контроль над максимальным и минимальным размером кучи.

Следующее исключительное условие связано с кучей:

  • Если для вычисления требуется больше памяти кучи, чем может предоставить система автоматического управления памятью, виртуальная машина Java генерирует исключение OutOfMemoryError.

2.5.4. Область методов

Виртуальная машина Java имеет область методов, которая совместно используется всеми потоками виртуальной машины Java. Область методов аналогична области хранения скомпилированного кода традиционного языка или аналогична сегменту «текст» в процессе операционной системы. Она хранит структуры на класс, такие как пул постоянных значений во время выполнения, данные о полях и методах и код методов и конструкторов, включая специальные методы, используемые при инициализации классов и интерфейсов и при инициализации экземпляров (§2.9).

Область методов создаётся при запуске виртуальной машины. Хотя область методов логически является частью кучи, простые реализации могут выбрать, не выполнять сборку мусора или уплотнение. Данная спецификация не предписывает расположение области методов или политики, используемые для управления скомпилированным кодом. Область методов может иметь фиксированный размер или может расширяться по мере необходимости для вычисления и может сжиматься, если более крупная область методов становится не нужна. Память для области методов не обязана быть непрерывной.

Реализация виртуальной машины Java может предоставить программисту или пользователю контроль над начальным размером области методов, а также, в случае области методов переменного размера, контроль над максимальным и минимальным размером области методов.

Следующее исключительное условие связано с областью методов:

  • Если память в области методов не может быть выделена для удовлетворения запроса на выделение, виртуальная машина Java генерирует исключение OutOfMemoryError.

2.5.5. Пул постоянных значений во время выполнения

Пул постоянных значений во время выполнения — это представление во время выполнения на класс или интерфейс таблицы constant_pool в файле class (§4.4). Он содержит несколько типов констант, от числовых литералов, известных во время компиляции, до ссылок на методы и поля, которые должны быть разрешены во время выполнения. Пул постоянных значений во время выполнения выполняет функцию, аналогичную таблице символов для традиционного языка программирования, хотя он содержит более широкий спектр данных, чем типичная таблица символов.

Каждый пул постоянных значений во время выполнения выделяется из области методов виртуальной машины Java (§2.5.4). Пул постоянных значений во время выполнения для класса или интерфейса создаётся при создании класса или интерфейса (§5.3) виртуальной машиной Java.

Следующее исключительное условие связано с созданием пула постоянных значений во время выполнения для класса или интерфейса:

  • При создании класса или интерфейса, если создание пула постоянных значений во время выполнения требует больше памяти, чем доступно в области методов виртуальной машины Java, виртуальная машина Java генерирует исключение OutOfMemoryError.

См. §5 (Загрузка, связывание и инициализация) для получения информации о создании пула постоянных значений во время выполнения.

2.5.6. Стек методов нативных вызовов

Реализация Java Virtual Machine может использовать стандартные стеки, разговорно называемые «C-стеками», для поддержки методов, написанных на языках, отличных от языка программирования Java. Стеки методов нативных вызовов также могут использоваться реализацией интерпретатора набора команд Java Virtual Machine на языке, таком как C. Реализации Java Virtual Machine, которые не могут загружать методы нативных вызовов и сами не полагаются на стандартные стеки, не обязаны предоставлять стеки методов нативных вызовов. Если они предоставляются, стеки методов нативных вызовов обычно выделяются на поток при создании каждого потока.

Этот спецификация допускает, что стеки методов нативных вызовов могут иметь фиксированный размер или динамически расширяться и сжиматься по мере необходимости для вычислений. Если стеки методов нативных вызовов имеют фиксированный размер, размер каждого стека можно выбирать независимо при создании этого стека.

Реализация Java Virtual Machine может предоставить программисту или пользователю контроль над начальным размером стеков методов нативных вызовов, а также, в случае стеков переменного размера, контроль над максимальным и минимальным размерами стека методов.

Следующие исключительные ситуации связаны со стеками методов нативных вызовов:

  • Если для вычислений в потоке требуется стек методов нативных вызовов больше, чем разрешено, Java Virtual Machine генерирует исключение StackOverflowError.

  • Если стеки методов нативных вызовов могут динамически расширяться, а попытка расширения стека приводит к недостатку памяти или недостаточно памяти для создания начального стека методов нативных вызовов для нового потока, Java Virtual Machine генерирует исключение OutOfMemoryError.

2.6. Фреймы

Фрейм используется для хранения данных и промежуточных результатов, а также для выполнения динамической связи, возвращения значений для методов и обработки исключений.

Каждый раз при вызове метода создаётся новый фрейм. Фрейм уничтожается, когда завершается его вызов, вне зависимости от того, нормальное это завершение или аномальное (выброшено неперехваченное исключение). Фреймы выделяются из стека виртуальной машины Java (§2.5.2) потока, создающего фрейм. Каждый фрейм имеет собственный массив локальных переменных (§2.6.1), свой собственный операционный стек (§2.6.2) и ссылку на пул постоянных времени выполнения (§2.5.5) класса текущего метода.

Фрейм может быть расширен дополнительной информацией, специфичной для реализации, например, информацией отладки.

Размеры массива локальных переменных и операционного стека определяются на этапе компиляции и предоставляются вместе с кодом метода, связанного с фреймом (§4.7.3). Таким образом, размер структуры данных фрейма зависит только от реализации виртуальной машины Java, и память для этих структур может быть выделена одновременно при вызове метода.

В любой момент времени в заданном потоке управления активен только один фрейм — фрейм выполняемого метода. Этот фрейм называется текущим фреймом, а его метод — текущим методом. Класс, в котором определён текущий метод, называется текущим классом. Операции с локальными переменными и операционным стеком, как правило, выполняются относительно текущего фрейма.

Фрейм перестаёт быть текущим, если его метод вызывает другой метод или если его метод завершается. При вызове метода создаётся новый фрейм, и он становится текущим, когда управление передаётся новому методу. При возврате из метода текущий фрейм возвращает результат вызова своего метода, если таковой имеется, предыдущему фрейму. Затем текущий фрейм удаляется, и предыдущий фрейм становится текущим.

Обратите внимание, что фрейм, созданный потоком, является локальным для этого потока и не может быть использован никаким другим потоком.

2.6.1. Локальные переменные

Каждый фрейм (§2.6) содержит массив переменных, известный как локальные переменные. Длина массива локальных переменных фрейма определяется на этапе компиляции и предоставляется в двоичном представлении класса или интерфейса вместе с кодом метода, связанного с фреймом (§4.7.3).

Одна локальная переменная может содержать значение типа 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.2. Операционные стеки

Каждый фрейм (§2.6) содержит стек, работающий по принципу LIFO (последним вошел — первым вышел), известный как операционный стек. Максимальная глубина операционного стека фрейма определяется на этапе компиляции и предоставляется вместе с кодом метода, связанного с фреймом (§4.7.3).

Когда это ясно из контекста, мы иногда будем ссылаться на операционный стек текущего фрейма просто как на операционный стек.

Операционный стек пуст, когда создаётся фрейм, который его содержит. Виртуальная машина Java предоставляет инструкции для загрузки констант или значений из локальных переменных или полей в операционный стек. Другие инструкции виртуальной машины берут операнды из операционного стека, выполняют операции над ними и записывают результат обратно в операционный стек. Операционный стек также используется для подготовки параметров, передаваемых методам, и для получения результатов методов.

Например, инструкция iadd (§iadd) складывает два значения типа int. Для этого требуются два верхних значения в операционном стеке, помещённые туда предыдущими инструкциями. Оба значения типа int извлекаются из операционного стека. Они складываются, а их сумма помещается обратно в операционный стек. Подвычисления могут быть вложены в операционном стеке, что приводит к значениям, которые могут быть использованы в рамках вычисления.

Каждая запись в операционном стеке может содержать значение любого типа виртуальной машины Java, включая значение типа long или типа double.

Значения из операционного стека должны обрабатываться способом, соответствующим их типам. Например, невозможно поместить два значения типа int и затем рассматривать их как значение типа long, или поместить два значения типа float и затем сложить их с помощью инструкции iadd. Небольшое число инструкций виртуальной машины Java (инструкции dup (§dup) и swap (§swap)) работают с областями данных времени выполнения как с сырыми значениями без учёта их конкретных типов; эти инструкции определены таким образом, что они не могут использоваться для изменения или разделения отдельных значений. Эти ограничения на работу с операционным стеком проверяются в процессе проверки файлов class (§4.10).

В любой момент времени операционный стек имеет связанную глубину, где значение типа long или double вносит два значения в глубину, а значение любого другого типа вносит одно значение.

2.6.3. Динамическая связь

Каждый фрейм (§2.6) содержит ссылку на пул постоянных времени выполнения (§2.5.5) для типа текущего метода, чтобы поддерживать динамическую связь кода метода. Код метода в файле class ссылается на вызываемые методы и переменные через символьные ссылки. Динамическая связь преобразует эти символьные ссылки на методы в конкретные ссылки на методы, загружая классы при необходимости для разрешения ещё не определённых символов и преобразует обращение к переменным в соответствующие смещения в структурах хранения, связанных с местоположением этих переменных во время выполнения.

Эта поздняя привязка методов и переменных делает изменения в других классах, используемых методом, менее вероятными, что повлияют на этот код.

2.6.4. Нормальное завершение вызова метода

Вызов метода завершается нормально, если этот вызов не вызывает исключение (§2.10) которое должно быть выброшено, либо напрямую виртуальной машиной Java, либо в результате выполнения явного оператора throw. Если вызов текущего метода завершается нормально, то может быть возвращено значение вызывающему методу. Это происходит, когда вызванный метод выполняет одну из инструкций возврата (§2.11.8), выбор которой должен быть уместным для типа возвращаемого значения (если таковое имеется).

В этом случае текущий фрейм (§2.6) используется для восстановления состояния вызывающего метода, включая его локальные переменные и операционный стек, при этом счётчик команд вызывающего метода увеличивается соответствующим образом, чтобы пропустить инструкцию вызова метода. Затем выполнение продолжается нормально в фрейме вызывающего метода с возвращаемым значением (если оно имеется) в стеке операционных данных этого фрейма.

2.6.5. Прерывание вызова метода

Вызов метода прерывается, если выполнение инструкции виртуальной машины Java внутри метода вызывает в виртуальной машине Java исключение (§2.10), и это исключение не обрабатывается внутри метода. Выполнение инструкции athrow (§athrow) также вызывает явное исключение, и если исключение не перехватывается текущим методом, это приводит к прерыванию вызова метода. Вызов метода, который завершается прерыванием, никогда не возвращает значение вызвавшему его методу.

2.7. Представление объектов

Виртуальная машина Java не навязывает какой-либо конкретной внутренней структуры для объектов.

В некоторых реализациях виртуальной машины Java Oracle ссылка на экземпляр класса является указателем на обработчик, который сам является парой указателей: один на таблицу, содержащую методы объекта, и указатель на Class объект, представляющий тип объекта, а другой — на выделенную из кучи память для данных объекта.

2.8. Арифметика с плавающей точкой

Виртуальная машина Java включает в себя подмножество арифметики с плавающей точкой, заданное стандартом IEEE 754 (JLS §1.7).

В Java SE 15 и более поздних версиях виртуальная машина Java использует версию стандарта IEEE 754 2019 года. До 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 для операции fusedMultiplyAdd IEEE 754 и метод 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 Platform разрешала реализации Java Virtual Machine иметь один или два набора значений для каждого типа с плавающей точкой. Тип float был связан с набором значений float и набором значений float с расширенным экспонентом, а тип double был связан с набором значений double и набором значений double с расширенным экспонентом. Набор значений float соответствовал значениям, представимым в формате IEEE 754 binary32; набор значений float с расширенным экспонентом имел то же количество битов точности, но больший диапазон экспоненты. Аналогично, набор значений double соответствовал значениям, представимым в формате IEEE 754 binary64; набор значений double с расширенным экспонентом имел то же количество битов точности, но больший диапазон экспоненты. Разрешение использования наборов значений с расширенным экспонентом по умолчанию смягчило проблемы производительности на некоторых семействах процессоров.

Для совместимости, Java 1.2 разрешала файл class запретить реализации использовать наборы значений с расширенным экспонентом. Файл class выражал это, устанавливая флаг ACC_STRICT в объявлении метода. ACC_STRICT ограничивало семантику с плавающей точкой инструкций метода использованием набора значений float для float операндов и набора значений double для double операндов, гарантируя, что результаты таких инструкций были полностью предсказуемы. Методы, помеченные как ACC_STRICT, таким образом, имели ту же семантику с плавающей точкой, что и в Java 1.0 и 1.1.

В Java SE 17 и более поздних версиях Java SE Platform всегда требует строгого вычисления выражений с плавающей точкой. Более новые члены семейств процессоров, у которых были проблемы с производительностью при реализации строгого вычисления, больше не испытывают этих трудностей. В этом спецификации больше не связывают float и double с четырьмя наборами значений, описанными выше, и флаг ACC_STRICT больше не влияет на вычисление операций с плавающей точкой. Для совместимости, битовая последовательность, назначенная для обозначения ACC_STRICT в файле class с номером основной версии 46-60, не назначена (то есть не обозначает какой-либо флаг) в файле class с номером основной версии больше 60 (§4.6). Будущие версии Java Virtual Machine могут назначить разное значение битовой последовательности в будущих файлах class.

2.9. Специальные методы

2.9.1. Методы инициализации экземпляра

У класса может быть ноль или более методов инициализации экземпляра, каждый из которых обычно соответствует конструктору, написанному на языке программирования 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.

2.9.2. Методы инициализации класса

Класс или интерфейс имеет не более одного метода инициализации класса или интерфейса, и инициализируется 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.

2.9.3. Методы полиморфизма сигнатуры

Метод является методом полиморфизма сигнатуры, если все следующие условия выполняются:

  • Он объявлен в классе 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, элементы массивов или компоненты внеheap структуры данных. Дополнительную информацию см. в пакете java.lang.invoke в API Java SE Platform.

2.10. Исключения

Исключение в Java Virtual Machine представляется экземпляром класса Throwable или одного из его подклассов. Бросок исключения приводит к немедленному нелокальному переходу управления из точки, в которой было брошено исключение.

Большинство исключений возникают синхронно в результате действия потока, в котором они происходят. В отличие от синхронных, асинхронные исключения могут возникнуть в любой момент выполнения программы. Java Virtual Machine бросает исключение по одной из трёх причин:

  • Была выполнена инструкция athrow (§athrow).

  • Java Virtual Machine синхронно обнаружила аномальное состояние выполнения. Такие исключения не выбрасываются в произвольной точке программы, а только синхронно после выполнения инструкции, которая:

    • Указывает исключение как возможный результат, например:

      • Когда инструкция выполняет операцию, нарушающую семантику языка программирования Java, например, индексирование за пределами границ массива.

      • Когда возникает ошибка при загрузке или связывании части программы.

    • Приводит к превышению лимита ресурса, например, при использовании слишком большого объёма памяти.

  • Произошло асинхронное исключение из-за внутренней ошибки в реализации Java Virtual Machine (§6.3).

Реализация Java Virtual Machine может позволить небольшой, но ограниченной по объёму части выполнения пройти, прежде чем будет брошено асинхронное исключение. Эта задержка позволяет оптимизированному коду обнаружить и бросить эти исключения в точках, где это удобно для их обработки, при соблюдении семантики языка программирования Java.

Простая реализация может проверять на наличие асинхронных исключений в каждой точке инструкции передачи управления. Поскольку программа имеет конечный размер, это задаёт предел общей задержки в обнаружении асинхронного исключения. Поскольку между передачами управления не произойдёт асинхронного исключения, генератор кода имеет некоторую гибкость в переупорядочении вычислений между передачами управления для повышения производительности. Статья Polling Efficiently on Stock Hardware Марка Феле, Proc. 1993 Conference on Functional Programming and Computer Architecture, Копенгаген, Дания, стр. 179–187, рекомендуется для дальнейшего изучения.

Исключения, брошенные Java Virtual Machine, точные: когда происходит передача управления, все эффекты инструкций, выполненных до точки броска исключения, должны казаться выполненными. Никакие инструкции, которые следуют за точкой броска исключения, не могут казаться оценёнными. Если оптимизированный код спекулятивно выполнил некоторые из инструкций, которые следуют за точкой возникновения исключения, такой код должен быть готов скрыть это спекулятивное выполнение от видимого для пользователя состояния программы.

Каждый метод в Java Virtual Machine может быть связан с нулём или более обработчиками исключений. Обработчик исключений определяет диапазон смещений в коде Java Virtual Machine, реализующем метод, для которого активен обработчик исключений, описывает тип исключения, который может обработать обработчик исключений, и указывает расположение кода, который должен обработать это исключение. Исключение соответствует обработчику исключений, если смещение инструкции, вызвавшей исключение, находится в диапазоне смещений обработчика исключений и тип исключения — тот же класс, что и у обработчика исключений, или его подкласс. При броске исключения Java Virtual Machine ищет соответствующий обработчик исключений в текущем методе. Если такой обработчик найден, система переходит к коду обработки исключения, указанному сопоставленным обработчиком.

Если такой обработчик исключений не найден в текущем методе, текущее вызов метода завершается внезапно (§2.6.5). При внезапном завершении стек операндов и локальные переменные текущего вызова метода удаляются, и его фрейм удаляется, восстанавливая фрейм вызывающего метода. Затем исключение перебрасывается в контексте фрейма вызывающего метода и так далее, продолжая по цепочке вызовов методов. Если подходящий обработчик исключений не будет найден до достижения вершины цепочки вызовов методов, выполнение потока, в котором было брошено исключение, завершается. Перед завершением потока необработанное исключение обрабатывается по следующим правилам:

  • Если у потока установлен обработчик необработанных исключений, то этот обработчик выполняется.

  • В противном случае вызывается метод uncaughtException для ThreadGroup, который является родителем потока. Если ThreadGroup и его родительские ThreadGroup не переопределяют uncaughtException, тогда вызывается метод по умолчанию uncaughtException.

Порядок поиска обработчиков исключений метода важен. В файле class обработчики исключений каждого метода хранятся в таблице (§4.7.3). Во время выполнения, при броске исключения, Java Virtual Machine ищет обработчики исключений текущего метода в порядке их появления в соответствующей таблице обработчиков исключений в файле class, начиная с начала этой таблицы.

Обратите внимание, что Java Virtual Machine не навязывает вложенность или какой-либо порядок записей таблицы исключений метода. Семантика обработки исключений языка программирования Java реализуется только путём сотрудничества с компилятором (§3.12). Когда файлы class генерируются другими способами, определённая процедура поиска гарантирует, что все реализации Java Virtual Machine будут вести себя согласованно.

2.11. Резюме набора команд

Инструкция Java Virtual Machine состоит из одного байта кода операции, указывающего на операцию, которую необходимо выполнить, за которым следуют ноль или более операндов, предоставляющих аргументы или данные, используемые операцией. Многие инструкции не имеют операндов и состоят только из кода операции.

Игнорируя исключения, внутренний цикл интерпретатора Java Virtual Machine эффективно

do {
    atomically calculate pc and fetch opcode at pc;
    if (operands) fetch operands;
    execute the action for the opcode;
} while (there is more to do);

Количество и размер операндов определяются кодом операции. Если операнд имеет размер больше одного байта, то он хранится в формате big-endian — старший байт первым. Например, 16-битный беззнаковый индекс в локальных переменных хранится как два беззнаковых байта, byte1 и byte2, так что его значение равно (byte1 << 8) | byte2.

Поток байткода инструкций выравнивается только по одному байту. Два исключения составляют инструкции lookupswitch и tableswitch (§lookupswitch, §tableswitch), которые дополняются, чтобы обеспечить внутреннее выравнивание некоторых из их операндов на 4-байтовых границах.

Решение ограничить код операции Java Virtual Machine одним байтом и отказаться от выравнивания данных в скомпилированном коде отражает сознательное предпочтение компактности, возможно, в ущерб производительности в простых реализациях. Однобайтовый код операции также ограничивает размер набора инструкций. Отсутствие предположения о выравнивании данных означает, что немедленные данные, превышающие один байт, должны быть построены из байтов во время выполнения на многих машинах.

2.11.1. Типы и Java Virtual Machine

Большинство инструкций в наборе команд Java Virtual Machine кодируют информацию о типах операций, которые они выполняют. Например, инструкция iload (§iload) загружает содержимое локальной переменной, которое должно быть int, в стек операндов. Инструкция fload (§fload) делает то же самое со значением float. Эти две инструкции могут иметь идентичные реализации, но имеют разные коды операций.

Для большинства типизированных инструкций тип инструкции представлен явно в мнемонике кода операции буквой: i для int операции, l для long, s для short, b для byte, c для char, f для float, d для double и a для reference. Некоторые инструкции, для которых тип однозначен, не имеют буквы типа в своей мнемонике. Например, arraylength всегда работает с объектом, который является массивом. Некоторые инструкции, такие как goto, безусловный переход управления, не работают с типизированными операндами.

Учитывая размер кода операции Java Virtual Machine в один байт, кодирование типов в коды операций создаёт давление на проектирование её набора инструкций. Если каждая типизированная инструкция поддерживала все типы данных времени выполнения Java Virtual Machine, инструкций было бы больше, чем можно представить в одном байте. Вместо этого набор инструкций Java Virtual Machine обеспечивает сокращённый уровень поддержки типов для определённых операций. Другими словами, набор инструкций намеренно не ортогонален. Для преобразования между неподдерживаемыми и поддерживаемыми типами данных по мере необходимости можно использовать отдельные инструкции.

Таблица 2.11.1-A обобщает поддержку типов в наборе инструкций Java Virtual Machine. Конкретная инструкция с информацией о типе строится путём замены T в шаблоне инструкции в столбце кода операции буквой из столбца типа. Если столбец типа для некоторого шаблона инструкции и типа пуст, то нет инструкции, поддерживающей этот тип операции. Например, есть инструкция загрузки для типа int, iload, но нет инструкции загрузки для типа byte.

Обратите внимание, что большинство инструкций в таблице 2.11.1-A не имеют форм для целочисленных типов byte, char и short. Ни одна не имеет форм для типа boolean. Всякий раз, когда значения типов byte и short загружаются в стек операндов, они неявно преобразуются с помощью расширения знака в значения типа int. Аналогично, всякий раз, когда значения типов boolean и char загружаются в стек операндов, они неявно преобразуются с помощью расширения нуля в значения типа 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 i2c
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.11.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.

Эта нотация для семейств инструкций используется на протяжении всего данного спецификационного документа.

2.11.3. Арифметические инструкции

Арифметические инструкции вычисляют результат, который обычно является функцией двух значений на стеке операндов, помещая результат обратно на стек операндов. Есть два основных типа арифметических инструкций: те, что работают с целыми значениями, и те, что работают с плавающей точкой. В каждом из этих типов арифметические инструкции специализированы для числовых типов виртуальной машины 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.

2.11.4. Инструкции преобразования типов

Инструкции преобразования типов позволяют преобразовывать числовые типы виртуальной машины 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 или long 0.

  • В противном случае, если значение с плавающей точкой не является бесконечностью, значение с плавающей точкой округляется до целочисленного значения V с помощью политики округления к нулю (§2.8). Есть два случая:

    • Если T — long и это целое значение может быть представлено как long, то результатом является значение long V.

    • Если T — типа int и это целое значение может быть представлено как int, то результатом является значение int V.

  • В противном случае:

    • Значение должно быть слишком малым (отрицательное значение большой величины или отрицательная бесконечность), и результатом является наименьшее представимое значение типа int или long.

    • Или значение должно быть слишком большим (положительное значение большой величины или положительная бесконечность), и результатом является наибольшее представимое значение типа int или long.

Сужающее числовое преобразование от double до float выполняется в соответствии с IEEE 754. Результат правильно округляется с использованием политики округления до ближайшего значения (§2.8). Значение, слишком малое для представления как float, преобразуется в положительный или отрицательный ноль типа float; значение, слишком большое для представления как float, преобразуется в положительную или отрицательную бесконечность. double NaN всегда преобразуется в float NaN.

Несмотря на то, что переполнение, недополнение или потеря точности могут произойти, сужающие преобразования между числовыми типами никогда не заставляют виртуальную машину Java выбрасывать исключение во время выполнения (не следует путать с исключением с плавающей точкой IEEE 754).

2.11.5. Создание и обработка объектов

Хотя и экземпляры классов, и массивы являются объектами, виртуальная машина 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.

2.11.6. Инструкции управления стеком операндов

Для непосредственного управления стеком операндов предоставляется ряд инструкций: pop, pop2, dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2, swap.

2.11.7. Инструкции передачи управления

Инструкции передачи управления условно или безусловно заставляют Java Virtual Machine продолжить выполнение с инструкции, отличной от следующей за инструкцией передачи управления. Это:

  • Условный переход: 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 Virtual Machine есть отдельные наборы инструкций, которые условно переходят по результатам сравнения с данными типов int и reference. Также существуют отдельные инструкции условного перехода, проверяющие на null, поэтому нет необходимости указывать конкретное значение для null (§2.4).

Условные переходы по результатам сравнений между данными типов boolean, byte, char и short выполняются с помощью инструкций сравнения int (§2.11.1). Условный переход по сравнению данных типов long, float или double инициируется инструкцией, сравнивающей данные и производящей int результат сравнения (§2.11.3). Следующая инструкция сравнения int проверяет этот результат и осуществляет условный переход. В связи со своим упором на int сравнения Java Virtual Machine предоставляет богатый набор инструкций условного перехода для типа int.

Все int инструкции условной передачи управления выполняют сравнения со знаком.

2.11.8. Инструкции вызова и возврата методов

Следующие пять инструкций вызывают методы:

  • invokevirtual вызывает виртуальный метод объекта, диспатчинг по (виртуальному) типу объекта.

  • invokeinterface вызывает интерфейсный метод, ищущий методы, реализованные конкретным объектом во время выполнения, чтобы найти соответствующий метод.

  • invokespecial вызывает метод экземпляра, требующий специальной обработки, либо метод инициализации экземпляра (§2.9.1), либо метод текущего класса или его супертипов.

  • invokestatic вызывает (static) статический метод в указанном классе.

  • invokedynamic вызывает метод, который является целевым объектом вызова, привязанным к инструкции invokedynamic. Объект вызова был привязан к конкретной лексической реализации инструкции invokedynamic Java Virtual Machine в результате запуска метода бутстрапа перед первым выполнением инструкции. Следовательно, каждая реализация инструкции invokedynamic имеет уникальное состояние связи, в отличие от других инструкций, которые вызывают методы.

Инструкции возврата метода, которые различаются по типу возвращаемого значения, являются ireturn (используется для типов возвращаемых значений boolean, byte, char, short и int), lreturn, freturn, dreturn и areturn. Кроме того, инструкция return используется для возврата из методов, объявленных как void, методов инициализации экземпляров и методов инициализации класса или интерфейса.

2.11.9. Выбрасывание исключений

Исключение выбрасывается программно с помощью инструкции athrow. Исключение также может быть вызвано различными инструкциями Java Virtual Machine, если они обнаружат аномальное состояние.

2.11.10. Синхронизация

Java Virtual Machine поддерживает синхронизацию как методов, так и последовательностей инструкций в пределах метода с помощью одного синхронизационного конструкта: monitor.

Синхронизация на уровне метода выполняется неявно, как часть вызова и возврата метода (§2.11.8). Метод synchronized отличается в структуре method_info постоянного пула времени выполнения (§4.6) флагом ACC_SYNCHRONIZED, который проверяется инструкциями вызова метода. При вызове метода, для которого установлен ACC_SYNCHRONIZED, выполняющая нить входит в монитор, вызывает сам метод и выходит из монитора, независимо от того, завершился ли вызов метода нормально или прерывисто. Пока выполняющая нить владеет монитором, никакая другая нить не может войти в него. Если во время вызова метода возникает исключение, и метод synchronized не обрабатывает исключение, монитор для метода автоматически закрывается перед тем, как исключение будет повторно выброшено из метода synchronized.

Синхронизация последовательностей инструкций обычно используется для кодирования блока synchronized языка программирования Java. Java Virtual Machine предоставляет инструкции monitorenter и monitorexit для поддержки таких конструкций языка. Правильная реализация блоков synchronized требует сотрудничества компилятора, нацеленного на Java Virtual Machine (§3.14).

Структурированное блокирование – это ситуация, когда во время вызова метода каждый выход из данного монитора соответствует предшествующему входу в этот монитор. Поскольку нет гарантии, что весь код, переданный Java Virtual Machine, будет выполнять структурированное блокирование, реализации Java Virtual Machine разрешено, но не обязательно, придерживаться двух следующих правил, гарантирующих структурированное блокирование. Пусть T – это нить, а M – это монитор. Тогда:

  1. Количество входов в монитор, выполненных нитью T в мониторе M во время вызова метода, должно быть равно количеству выходов из монитора, выполненных нитью T в мониторе M во время вызова метода, независимо от того, завершился ли вызов метода нормально или прерывисто.

  2. Ни в какой момент во время вызова метода количество выходов из монитора, выполненных нитью T в мониторе M с момента вызова метода, не должно превышать количество входов в монитор, выполненных нитью T в мониторе M с момента вызова метода.

Обратите внимание, что вход и выход из монитора, автоматически выполненные Java Virtual Machine при вызове метода synchronized, считаются выполняющимися во время вызова вызывающего метода.

2.12. Библиотеки классов

Виртуальная машина Java должна обеспечивать достаточную поддержку для реализации библиотек классов платформы Java SE. Некоторые классы в этих библиотеках не могут быть реализованы без взаимодействия с виртуальной машиной Java.

К классам, которые могут потребовать особой поддержки от виртуальной машины Java, относятся те, что поддерживают:

  • Рефлексию, такие как классы в пакете java.lang.reflect и класс Class.

  • Загрузку и создание класса или интерфейса. Наиболее очевидным примером является класс ClassLoader.

  • Связывание и инициализацию класса или интерфейса. Приведенные выше примеры классов также попадают в эту категорию.

  • Систему модулей, такую как класс ModuleLayer.

  • Многопоточность, такую как класс Thread.

  • Слабые ссылки, такие как классы в пакете java.lang.ref.

Данный список призван быть иллюстративным, а не исчерпывающим. Полный список этих классов или функциональности, которую они предоставляют, выходит за рамки этой спецификации. Подробную информацию см. в спецификациях библиотек классов платформы Java SE.

2.13. Публичный дизайн, закрытая реализация

До сих пор эта спецификация описывала публичный вид виртуальной машины 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.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API