3. Набор команд JVM

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

Без учёта исключений, внутренний цикл интерпретатора виртуальной Java-машины имеет следующий вид:

Рис.8. Алгоритм интерпретатора байткода.

Количество и размер операндов определяется кодом операции. Если дополнительный операнд содержит более одного байта, то они хранятся в порядке от старшего к младшему - старший байт находится по младшему адресу (big-endian). Например, беззнаковый 16-битный параметр хранится как два байта, его значение вычисляется как:

(байт1 << 8) | байт2

Рис.9. big-endian.

Рис.10. little-endian.

3.1. Типы данных и набор команд JVM

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

Для большинства типизированных инструкций, тип явно указывается первой буквой мнемоники команды:

Таблица 2. Буква мнемоники и тип данных команды.

'i' int
'l' long
's' short
'b' byte
'c' char
'f' float
'd' double
'a' reference

Команды, для которых тип не вызывает сомнения, не содержат буквы типа в своих мнемониках. Например, команда arraylength всегда работает с объектом, который является массивом. Некоторые команды, такие как goto (безусловный переход), не работают с типизированными операндами.

Так как под код операции в формате команд JVM отводится 1 байт, то закодировать все типы данных в системе команд было бы просто невозможно. Разработчики Java решили эту проблему следующим образом: команды работают с ограниченным списком типов, а для работы с оставшимися типами используются команды преобразования типов.

Таблица 3. Команды JVM и поддерживаемые ими типы данных.

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         fcmpg dcmpg    
if_TcmpOP     if_icmpOP         if_acmpOP
Treturn     ireturn lreturn freturn dreturn   areturn

Представленная таблица иллюстрирует поддержку типов в наборе команд виртуальной машины Java. Конкретная команда с информацией о типе получается путём замены T в шаблоне команды из левого столбца на букву, соответствующую типу столбца. Если ячейка пуста, то это означает, что не существует инструкции, которая напрямую работает с данным типом. Например, существует инструкция load, работающая с типом int, но нет аналогичной команды, которая работала бы с типом byte.

Обратите внимание, что большинство инструкций не имеют соответствующих форм для работы с целыми типами byte, char и short, и ни одна инструкция не работает с типом boolean. Для поддержки этих типов осуществляется их расширение до типа int на этапе компиляции или исполения Java-программы. Причём для типов byte и short используется расширение знаком (рис.11), а для типов boolean и char - расширение нулём (рис.12). Реально операции с такими переменными осуществляются при помощи команд, работающих с типом int.

Рис.11. Расширение знаком.

Рис.12. Расширение нулём.

Некоторые команды JVM, такие как pop и swap, работают со стеком операндов, не обращая внимания на тип; однако, такие команды ограничены в использовании только над переменными определённых категорий вычислительных типов.

Соответствие реальных и вычислительных типов виртуальной машины Java представлено следующей таблицей:

Таблица 4. Реальные и вычислительные типы JVM.

Реальный тип Вычислительный тип Количество ячеек стека
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

3.2. Команды загрузки и сохранения

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

  • Загрузка локальных переменных в стек операндов: 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, supush, ldc, ldc_w, ldc2_w, aconst_null, iconst_m1, iconst_<i>, lconst_<l>, fconst_<f>, dconst_<d>.
  • Получение доступа к большему числу локальных переменных, засчёт использования расширенного индекса, или к большему непосредственному операнду: wide.

Перечисленные выше мнемоники команд, содержащие в конце букву между двумя угловыми скобками(например, 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.

3.3. Арифметические и логические команды

Арифметические команды вычисляют результат, который обычно является функцией двух значений на стеке операндов, помещая результат обратно на стек операндов. Существуют два основных типа арифметических команд: работающие с целочисленными значениями и работающие со значениями с плавающей точкой. В системе команд нет прямой поддежки арифметических и логических операций с типами byte, short, char и 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.
  • Поразрядное исключающее ИЛИ (сумма по модулю 2): ixor, lxor.
  • Сравнение: dcmpg, dcmpl, fcmpg, fcmpl, lcmp.

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

Виртуальная машина Java не сигнализирует о возникновении переполнения при выполнении операций над целочисленными типами данных. Единственные целочисленные операции, которые могут вызвать возникновение исключения - это деление (idiv и ldiv) и нахождение остатка от деления (irem и lrem), которые генерируют исключение ArithmeticException, если делитель равен нулю.

3.4. Команды преобразования типов

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

Виртуальная машина Java напрямую поддерживает следующие расширяющие численные преобразования типов:

  • int в long, float или double (i2l, i2f, i2d)
  • long в float или double (l2f, l2d)
  • float в double (f2d)

При преобразовании int или long в float, а также long в double возможна потеря точности. Обратите внимание, что не существует команд преобразования из типов byte, char, и short в тип int. Эти преобразования осуществляются неявно на этапе компиляции и при работе виртуальной машины Java.

JVM также непосредственно осуществляет следующие сужающие преобразования типов:

  • int в byte, short или char (i2b, i2s, i2c)
  • long в int (l2i)
  • float в int или long (f2i, f2l)
  • double в int, long или float (d2i, d2l, d2f)

Сужающие преобразования типов могут привести к потере не только точности представления числа, но также к изменению знака числа. Сужающие преобразования из типа int или long выполняются простым отбрасывание всех битов числа кроме N младших битов, где N - количество битов в представлении типа, являющегося результатом преобразования.

3.5. Команды работы с объектами

Объектами в Java являются как экземпляры классов, так и массивы. Виртуальная машина Java использует определённые команды для создания экземпляров классов и массивов:

  • Создание нового экземпляра класса: new.
  • Создание нового массива: newarray, anewarray, multianewarray.
  • Доступ к полям классов (статические поля) и полям экземпляров классов (нестатические поля): getfield, putfield, getstatic, putstatic.
  • Загрузка элемента массива в стек операндов: baload, caload, saload, iaload, laload, faload, daload, aaload.
  • Сохранение элемента массива из стека операндов: bastore, castore, sastore, iastore, lastore, fastore, dastore, aastore.
  • Получение длины массива: arraylength.
  • Проверка свойств экземпляра класса или массива: instanceof, checkcast.

3.6. Команды управления стеком операндов

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

3.7. Команды передачи управления

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

  • Условный переход: ifeq, iflt, ifle, ifne, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_icmpgt, if_icmple, if_icmpge, if_acmpeq, if_acmpne.
  • Составной условный переход: tableswitch, lookupswitch.
  • Безусловный переход: goto, goto_w, jsr, jsr_w, ret.

Виртуальная машина Java поддерживает специализированные команды, которые осуществляют условный переход в зависимости от результата сравнения величин типа int или reference. Также есть команды для условного перехода в зависимости от результата проверки на нулевую ссылку.

Условные переходы по результату сравнения величин типов boolean, byte, char, short осуществляются с помощью команд, работающих с типом int. Условный переход по результатм сравнения величин типов long, float, double инициализируется с помощью команды, которая сравнивает величины и генерирует результат сравнения, который представляет собой переменную типа int. Последующая команда условного перехода проверяет значение результата сравнения и выполняет или не выполняет переход.

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

3.8. Команды вызова методов и возврата управления

JVM поддерживает 4 команды для вызова методов:

  • invokevirtual вызывает метод экземпляра класса, в зависимости от типа объекта. Это обычный вызов метода в языке программирования Java.
  • invokeinterface вызывает метод, который реализуется интерфейсом. Осуществляется поиск методов, реализованных определённым объектом, чтобы найти подходящий метод.
  • invokespecial вызывает метод экземпляра класса, требующий специальной обработки, например метод инициализации экземпляра класса, private-метод или метод суперкласса.
  • invokestatic вызывает метод класса (статический метод).

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

3.9. Генерация исключений

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

Наверх | Дальше
Королёв А.В. ИС-310 2006г.