Глава 3. Компиляция для виртуальной машины Java
Оглавление
- 3.1. Формат примеров
- 3.2. Использование констант, локальных переменных и управляющих конструкций
- 3.3. Арифметика
- 3.4. Доступ к пулу постоянных времени выполнения
- 3.5. Более сложные примеры управления
- 3.6. Прием аргументов
- 3.7. Вызов методов
- 3.8. Работа с экземплярами классов
- 3.9. Массивы
- 3.10. Компиляция переключателей
- 3.11. Операции со стеком операндов
- 3.12. Бросание и обработка исключений
- 3.13. Компиляция
finally - 3.14. Синхронизация
- 3.15. Аннотации
- 3.16. Модули
Виртуальная машина Java разработана для поддержки языка программирования Java. Программное обеспечение JDK компании Oracle содержит компилятор из исходного кода, написанного на языке программирования Java, в набор инструкций виртуальной машины Java, а также среду выполнения, которая реализует саму виртуальную машину Java. Понимание того, как один компилятор использует виртуальную машину Java, полезно как потенциальному автору компилятора, так и тому, кто пытается понять саму виртуальную машину Java. Нумерованные разделы в этой главе не являются нормативными.
Обратите внимание, что термин «компилятор» иногда используется применительно к переводчику из набора инструкций виртуальной машины Java в набор инструкций конкретного процессора. Одним примером такого переводчика является генератор кода Just-in-Time (JIT), который генерирует инструкции, специфичные для платформы, только после загрузки кода виртуальной машины Java. В этой главе не рассматриваются вопросы, связанные с генерацией кода, только те, что связаны с компиляцией исходного кода, написанного на языке программирования Java, в инструкции виртуальной машины Java.
Эта глава в основном состоит из примеров исходного кода вместе с аннотированными списками кода виртуальной машины Java, который компилятор javac в выпуске JDK 1.0.2 компании Oracle генерирует для этих примеров. Код виртуальной машины Java написан на неофициальном «языке ассемблера виртуальной машины», выводимом утилитой javap компании Oracle, распространяемой с выпуском JDK. Вы можете использовать javap для генерации дополнительных примеров скомпилированных методов.
Формат примеров должен быть знаком всем, кто читал код ассемблера. Каждая инструкция имеет вид:
<index> <opcode> [ <operand1> [ <operand2>... ]] [<comment>]
<index> — это индекс операционного кода инструкции в массиве, который содержит байты кода виртуальной машины для этого метода. В качестве альтернативы, <index> можно рассматривать как смещение байтов от начала метода. <opcode> — мнемоника для операционного кода инструкции, а ноль или более <operandN> — операнды инструкции. Необязательная <comment> приводится в формате комментария в конце строки:
8 bipush 100 // Push int constant 100
Часть информации в комментариях исходит от javap; остальное — от авторов. <index>, предваряющая каждую инструкцию, может использоваться в качестве целевой точки инструкции передачи управления. Например, инструкция goto
8 передает управление инструкции с индексом 8. Обратите внимание, что фактические операнды инструкций передачи управления виртуальной машины — это смещения относительно адресов операционных кодов этих инструкций; эти операнды отображаются javap (и показаны в этой главе) в виде более легко читаемых смещений в методах.
Мы предваряем операнд, представляющий индекс пула постоянных времени выполнения, знаком решетки и сопровождаем инструкцию комментарием, идентифицирующим элемент пула постоянных времени выполнения, на который ссылаются, как в:
10 ldc #1 // Pushfloatconstant100.0
или:
9 invokevirtual #4 // Method Example.addTwo(II)I
В целях этой главы мы не будем углубляться в детали, такие как размер операндов.
Код виртуальной машины Java обладает набором общих характеристик, навязанных проектированием виртуальной машины Java и использованием типов. В первом примере мы сталкиваемся со многими из них, и мы рассмотрим их подробно.
Метод spin просто выполняет пустой цикл for 100 раз:
void spin() {
int i;
for (i = 0; i < 100; i++) {
; // Loop body is empty
}
}
Компилятор может скомпилировать spin в:
0 iconst_0 // Push int constant 0 1 istore_1 // Store into local variable 1 (i=0) 2 goto 8 // First time through don't increment 5 iinc 1 1 // Increment local variable 1 by 1 (i++) 8 iload_1 // Push local variable 1 (i) 9 bipush 100 // Push int constant 100 11 if_icmplt 5 // Compare and loop if less than (i < 100) 14 return // Return void when done
Виртуальная машина Java имеет стековый характер, причём большинство операций берут один или несколько операндов со стека операндов текущей рамки виртуальной машины Java или помещают результаты обратно в стек операндов. Каждая вызов метода создает новую рамку, вместе с ней создается новый стек операндов и набор локальных переменных для использования данным методом (§2.6). В любой момент вычисления, таким образом, могут быть много рамок и столько же стеков операндов на каждый поток управления, что соответствует множеству вложенных вызовов методов. Только стек операндов в текущей рамке активен.
Набор инструкций виртуальной машины Java различает типы операндов, используя отдельные байткоды для операций над различными типами данных. Метод spin работает только со значениями типа int. Инструкции в его скомпилированном коде, выбранные для работы с типизированными данными (iconst_0, istore_1, iinc, iload_1, if_icmplt) все специализированы для типа int.
Две константы в spin, 0 и 100, помещаются в стек операндов с помощью двух разных инструкций. 0 помещается с помощью инструкции iconst_0, одной из семейства инструкций iconst_<i>. 100 помещается с помощью инструкции bipush, которая извлекает значение, которое она помещает, как непосредственный операнд.
Виртуальная машина Java часто использует вероятность определенных операндов (int констант -1, 0, 1, 2, 3, 4 и 5 в случае с инструкциями iconst_<i>) сделав эти операнды неявными в коде операции. Так как инструкция iconst_0 знает, что она собирается поместить целое число int 0, iconst_0 не нужно хранить операнд, чтобы сказать ей, какое значение поместить, ни извлекать или декодировать операнд. Скомпилировать помещение 0 как bipush 0 было бы правильно, но сделало бы скомпилированный код для spin на один байт длиннее. Простая виртуальная машина также потратила бы дополнительное время на извлечение и декодирование явного операнда каждый раз при повторении цикла. Использование неявных операндов делает скомпилированный код более компактным и эффективным.
Локальная переменная int i в spin хранится как локальная переменная 1 виртуальной машины Java. Поскольку большинство инструкций виртуальной машины Java оперируют значениями, извлеченными из стека операндов, а не непосредственно локальными переменными, инструкции, которые передают значения между локальными переменными и стеком операндов, являются распространенными в коде, скомпилированном для виртуальной машины Java. Эти операции также имеют специальную поддержку в наборе инструкций. В spin значения передаются в локальные переменные и из них с помощью инструкций istore_1 и iload_1, каждая из которых неявно работает с локальной переменной 1. Инструкция istore_1 извлекает целое число int со стека операндов и сохраняет его в локальной переменной 1. Инструкция iload_1 помещает значение из локальной переменной 1 в стек операндов.
Использование (и повторное использование) локальных переменных является обязанностью автора компилятора. Специализированные инструкции загрузки и сохранения должны побудить автора компилятора повторно использовать локальные переменные, насколько это возможно. Полученный код работает быстрее, компактнее и использует меньше места в рамке.
Определенные очень частые операции с локальными переменными обрабатываются специально виртуальной машиной Java. Инструкция iinc увеличивает содержимое локальной переменной на значение со знаком размером в один байт. Инструкция iinc в spin увеличивает первую локальную переменную (ее первый операнд) на 1 (ее второй операнд). Инструкция iinc очень удобна при реализации циклических конструкций.
Цикл for в spin выполняется в основном этими инструкциями:
5 iinc 1 1 // Increment local variable 1 by 1 (i++) 8 iload_1 // Push local variable 1 (i) 9 bipush 100 // Push int constant 100 11 if_icmplt 5 // Compare and loop if less than (i < 100)
Инструкция bipush помещает значение 100 в стек операндов в качестве int, а затем инструкция if_icmplt извлекает это значение со стека операндов и сравнивает его с i. Если сравнение успешно (переменная i меньше 100), управление передается в индекс 5 и начинается следующий цикл for. В противном случае управление передается инструкции, следующей за if_icmplt.
Если в примере spin для счетчика цикла был использован тип данных, отличный от int, скомпилированный код обязательно изменится, чтобы отразить разный тип данных. Например, если вместо целого числа int пример spin использует double, как показано:
void dspin() {
double i;
for (i = 0.0; i < 100.0; i++) {
; // Loop body is empty
}
}
скомпилированный код:
Method void dspin() 0 dconst_0 // Push double constant 0.0 1 dstore_1 // Store into local variables 1 and 2 2 goto 9 // First time through don't increment 5 dload_1 // Push local variables 1 and 2 6 dconst_1 // Push double constant 1.0 7 dadd // Add; there is no dinc instruction 8 dstore_1 // Store result in local variables 1 and 2 9 dload_1 // Push local variables 1 and 2 10 ldc2_w #4 // Push double constant 100.0 13 dcmpg // There is no if_dcmplt instruction 14 iflt 5 // Compare and loop if less than (i < 100.0) 17 return // Return void when done
Инструкции, работающие с типизированными данными, теперь специализированы для типа double. (Инструкция ldc2_w будет обсуждена позже в этой главе.)
Вспомним, что значения double занимают две локальные переменные, хотя к ним обращаются только используя меньший индекс из двух локальных переменных. Это также относится к значениям типа long. Например,
double doubleLocals(double d1, double d2) {
return d1 + d2;
}
становится
Method double doubleLocals(double,double) 0 dload_1 // First argument in local variables 1 and 2 1 dload_3 // Second argument in local variables 3 and 4 2 dadd 3 dreturn
Обратите внимание, что локальные переменные пар локальных переменных, используемых для хранения значений типа double в doubleLocals, никогда не должны манипулироваться индивидуально.
Размер кода операции виртуальной машины Java в 1 байт приводит к тому, что он очень компактный. Однако 1-байтовые коде операции означают, что набор инструкций виртуальной машины Java должен оставаться небольшим. В качестве компромисса виртуальная машина Java не предоставляет одинаковой поддержки для всех типов данных: она не полностью ортогональна (Таблица 2.11.1-A).
Например, сравнение значений типа int в операторе for примера spin может быть реализовано с помощью одной инструкции if_icmplt; однако, нет одной инструкции в наборе инструкций виртуальной машины Java, которая выполняет условный переход на значениях типа double. Таким образом, dspin должен реализовать сравнение значений типа double с помощью инструкции dcmpg, за которой следует инструкция iflt.
Виртуальная машина Java предоставляет наибольшую прямую поддержку данных типа int. Это частично обусловлено ожиданием эффективных реализаций стеков операндов и массивов локальных переменных виртуальной машины Java. Это также обусловлено частотой данных типа int в типичных программах. Другие целочисленные типы имеют менее прямую поддержку. Например, нет инструкций short, char или byte для операций сохранения, загрузки или сложения. Вот пример spin, написанный с использованием short:
void sspin() {
short i;
for (i = 0; i < 100; i++) {
; // Loop body is empty
}
}
Он должен быть скомпилирован для виртуальной машины Java следующим образом, используя инструкции, работающие с другим типом, скорее всего int, преобразуя значения short и int по мере необходимости, чтобы гарантировать, что результаты операций над данными short остаются в пределах соответствующего диапазона:
Method void sspin() 0 iconst_0 1 istore_1 2 goto 10 5 iload_1 // The short is treated as though an int 6 iconst_1 7 iadd 8 i2s // Truncate int to short 9 istore_1 10 iload_1 11 bipush 100 13 if_icmplt 5 16 return
Отсутствие прямой поддержки типов byte, char и short в виртуальной машине Java не является значительной проблемой, потому что значения этих типов неявно преобразуются к типу int (byte и short расширяются со знаком до int, char расширяется с нулем). Таким образом, операции с данными byte, char и short могут быть выполнены с помощью инструкций int. Единственная дополнительная стоимость — это усечение значений операций с int до допустимых диапазонов.
Тип данных с плавающей точкой имеют промежуточный уровень поддержки в виртуальной машине Java, не хватает только полного набора инструкций условного перехода.
Виртуальная машина Java обычно выполняет арифметические операции на стеке операндов. (Исключением является инструкция iinc, которая напрямую увеличивает значение локальной переменной.) Например, метод align2grain выравнивает значение int до заданной степени двойки:
int align2grain(int i, int grain) {
return ((i + grain-1) & ~(grain-1));
}
Операнды для арифметических операций извлекаются из стека операндов, а результаты операций возвращаются обратно в стек операндов. Результаты промежуточных арифметических вычислений могут использоваться как операнды в последующих вычислениях. Например, вычисление ~(grain-1) обрабатывается с помощью следующих инструкций:
5 iload_2 // Push grain 6 iconst_1 // Push int constant 1 7 isub // Subtract; push result 8 iconst_m1 // Push int constant -1 9 ixor // Do XOR; push result
Сначала grain-1 рассчитывается с использованием содержимого локальной переменной 2 и непосредственного значения int, 1. Эти операнды извлекаются из стека операндов, а их разность возвращается в стек операндов. Разность сразу же доступна для использования как один из операндов инструкции ixor. (Напомним, что ~x ==
-1^x.) Аналогично, результат инструкции ixor становится операндом для последующей инструкции iand.
Следующий код представляет собой весь метод:
Method int align2grain(int,int) 0 iload_1 1 iload_2 2 iadd 3 iconst_1 4 isub 5 iload_2 6 iconst_1 7 isub 8 iconst_m1 9 ixor 10 iand 11 ireturn
Многие числовые константы, а также объекты, поля и методы, доступны через пул констант во время выполнения текущего класса. Доступ к объектам рассматривается позже (§3.8). Данные типов int, long, float и double, а также ссылки на экземпляры класса String, управляются с помощью инструкций ldc, ldc_w и ldc2_w.
Инструкции ldc и ldc_w используются для доступа к значениям в пуле констант во время выполнения (включая экземпляры класса String) типов, отличных от double и long. Инструкция ldc_w используется вместо ldc только при большом количестве элементов пула констант во время выполнения и необходимости доступа к элементу по более высокому индексу. Инструкция ldc2_w используется для доступа ко всем значениям типов double и long; нет варианта без расширения.
Целочисленные константы типов byte, char или short, а также небольшие значения int, могут быть скомпилированы с помощью инструкций bipush, sipush или iconst_<i> (§3.2). Некоторые небольшие константы с плавающей точкой могут быть скомпилированы с помощью инструкций fconst_<f> и dconst_<d>.
Во всех этих случаях компиляция проста. Например, константы для:
void useManyNumeric() {
int i = 100;
int j = 1000000;
long l1 = 1;
long l2 = 0xffffffff;
double d = 2.2;
...do some calculations...
}
настраиваются следующим образом:
Method void useManyNumeric() 0 bipush 100 // Push small int constant with bipush 2 istore_1 3 ldc #1 // Push large int constant (1000000) with ldc 5 istore_2 6 lconst_1 // A tiny long value uses small fast lconst_1 7 lstore_3 8 ldc2_w #6 // Push long 0xffffffff (that is, an int -1) // Any long constant value can be pushed with ldc2_w 11 lstore 5 13 ldc2_w #8 // Push double constant 2.200000 // Uncommon double values are also pushed with ldc2_w 16 dstore 7 ...do those calculations...
Компиляция инструкций for была показана в предыдущем разделе (§3.2). Большинство других управляющих конструкций языка программирования Java (if-then-else, do, while, break и continue) также компилируются очевидным образом. Компиляция инструкций switch обрабатывается в отдельном разделе (§3.10), как и компиляция исключений (§3.12) и компиляция блоков finally (§3.13).
В качестве дополнительного примера цикл while компилируется очевидным образом, хотя конкретные инструкции передачи управления, предоставляемые виртуальной машиной Java, различаются в зависимости от типа данных. Как обычно, есть больше поддержки данных типа int, например:
void whileInt() {
int i = 0;
while (i < 100) {
i++;
}
}
компилируется в:
Method void whileInt() 0 iconst_0 1 istore_1 2 goto 8 5 iinc 1 1 8 iload_1 9 bipush 100 11 if_icmplt 5 14 return
Обратите внимание, что проверка условия while (реализованная с помощью инструкции if_icmplt) находится внизу кода виртуальной машины Java для цикла. (Это также было в примерах spin ранее.) Нахождение проверки в конце цикла вынуждает использование инструкции goto для возвращения к проверке до первой итерации цикла. Если проверка не выполняется, и тело цикла не выполняется, эта дополнительная инструкция тратится. Однако циклы while обычно используются, когда ожидается, что их тело будет выполняться, часто много раз. Для последующих итераций размещение проверки в конце цикла экономит инструкцию виртуальной машины Java при каждом прохождении цикла: если проверка была бы в начале цикла, тело цикла потребовало бы заключительной инструкции goto для возвращения к началу.
Управляющие конструкции, включающие другие типы данных, компилируются аналогичным образом, но должны использовать доступные инструкции для этих типов данных. Это приводит к несколько менее эффективному коду, поскольку требуется больше инструкций виртуальной машины Java, например:
void whileDouble() {
double i = 0.0;
while (i < 100.1) {
i++;
}
}
компилируется в:
Method void whileDouble() 0 dconst_0 1 dstore_1 2 goto 9 5 dload_1 6 dconst_1 7 dadd 8 dstore_1 9 dload_1 10 ldc2_w #4 // Push double constant 100.1 13 dcmpg // To compare and branch we have to use... 14 iflt 5 // ...two instructions 17 return
Каждый тип с плавающей точкой имеет две инструкции сравнения: fcmpl и fcmpg для типа float и dcmpl и dcmpg для типа double. Варианты различаются только обработкой NaN. NaN не упорядочен (§2.3.2), поэтому все сравнения с плавающей точкой завершаются неудачей, если один из операндов является NaN. Компилятор выбирает вариант инструкции сравнения для соответствующего типа, который дает тот же результат, независимо от того, завершилось ли сравнение неудачей для значений, отличных от NaN, или столкнулось с NaN. Например:
int lessThan100(double d) {
if (d < 100.0) {
return 1;
} else {
return -1;
}
}
компилируется в:
Method int lessThan100(double) 0 dload_1 1 ldc2_w #4 // Push double constant 100.0 4 dcmpg // Push 1 if d is NaN or d > 100.0; // push 0 if d == 100.0 5 ifge 10 // Branch on 0 or 1 8 iconst_1 9 ireturn 10 iconst_m1 11 ireturn
Если d не NaN и меньше 100.0, инструкция dcmpg помещает значение int -1 в стек операндов, а инструкция ifge не выполняет перехода. Если d больше 100.0 или является NaN, инструкция dcmpg помещает значение int 1 в стек операндов, а инструкция ifge выполняет переход. Если d равно 100.0, инструкция dcmpg помещает значение int 0 в стек операндов, а инструкция ifge выполняет переход.
Инструкция dcmpl достигает того же результата, если сравнение обратное:
int greaterThan100(double d) {
if (d > 100.0) {
return 1;
} else {
return -1;
}
}
становится:
Method int greaterThan100(double) 0 dload_1 1 ldc2_w #4 // Push double constant 100.0 4 dcmpl // Push -1 if d is NaN or d < 100.0; // push 0 if d == 100.0 5 ifle 10 // Branch on 0 or -1 8 iconst_1 9 ireturn 10 iconst_m1 11 ireturn
Еще раз, независимо от того, завершилось ли сравнение неудачей для значения, отличного от NaN, или потому, что ему был передан NaN, инструкция dcmpl помещает значение int в стек операндов, что заставляет инструкцию ifle выполнить переход. Если обе инструкции dcmp не существовали, одному из методов примера пришлось бы выполнить больше работы для обнаружения NaN.
Если методу-экземпляру передаётся n аргументов, они, по соглашению, принимаются в локальных переменных с номерами 1 по n в кадре, созданном для нового вызова метода. Аргументы принимаются в том порядке, в котором они были переданы. Например:
int addTwo(int i, int j) {
return i + j;
}
компилируется в:
Method int addTwo(int,int) 0 iload_1 // Push value of local variable 1 (i) 1 iload_2 // Push value of local variable 2 (j) 2 iadd // Add; leave int result on operand stack 3 ireturn // Return int result
По соглашению, методу-экземпляру передаётся ссылка на его экземпляр в локальной переменной 0. В языке программирования Java экземпляр доступен через ключевое слово this.
Методы класса (static) не имеют экземпляра, поэтому это использование локальной переменной 0 для них излишне. Метод класса начинает использовать локальные переменные с индекса 0. Если метод addTwo был методом класса, его аргументы были бы переданы аналогичным образом, как в первом варианте:
static int addTwoStatic(int i, int j) {
return i + j;
}
компилируется в:
Method int addTwoStatic(int,int) 0 iload_0 1 iload_1 2 iadd 3 ireturn
Единственное отличие состоит в том, что аргументы метода появляются, начиная с локальной переменной 0, а не 1.
Обычный вызов метода экземпляра происходит с помощью типа объекта во время выполнения. (Они являются виртуальными, в терминах C++.) Такой вызов реализуется с помощью инструкции invokevirtual, которая принимает в качестве аргумента индекс в записи пула постоянных времени выполнения, предоставляющей внутреннюю форму двоичного имени типа класса объекта, имя вызываемого метода и его дескриптор (§4.3.3). Для вызова метода addTwo, определенного ранее как метод экземпляра, можно написать:
int add12and13() {
return addTwo(12, 13);
}
Это компилируется в:
Method int add12and13() 0 aload_0 // Push local variable 0 (this) 1 bipush 12 // Push int constant 12 3 bipush 13 // Push int constant 13 5 invokevirtual #4 // Method Example.addtwo(II)I 8 ireturn // Return int on top of operand stack; // it is the int result of addTwo()
Вызов устанавливается путём предварительного помещения ссылки на текущий экземпляр, reference, в стек операндов. Затем в стек операндов помещаются аргументы вызова метода, int значения 12 и 13. Когда создаётся кадр для метода addTwo, аргументы, переданные в метод, становятся начальными значениями локальных переменных нового кадра. То есть, значение reference для this и два аргумента, помещённые в стек операндов вызывающим методом, станут начальными значениями локальных переменных 0, 1 и 2 вызываемого метода.
Наконец, вызывается addTwo. При возвращении его значение возврата int помещается в стек операндов кадра вызывающего метода, метода add12and13. Таким образом, значение возврата подготавливается для немедленного возвращения вызывающему методу add12and13.
Возврат из add12and13 обрабатывается инструкцией ireturn метода add12and13. Инструкция ireturn принимает значение возврата int, возвращённое методом addTwo, из стека операндов текущего кадра и помещает его в стек операндов кадра вызывающего метода. Затем она возвращает управление вызывающему методу, делая кадр вызывающего метода текущим. Java Virtual Machine предоставляет отдельные инструкции возврата для многих числовых и reference типов данных, а также инструкцию return для методов без значения возврата. Один и тот же набор инструкций возврата используется для всех разновидностей вызова методов.
Операнд инструкции invokevirtual (в примере, индекс пула постоянных времени выполнения #4) не является смещением метода в экземпляре класса. Компилятор не знает внутреннюю структуру экземпляра класса. Вместо этого он генерирует символические ссылки на методы экземпляра, которые хранятся в пуле постоянных времени выполнения. Эти элементы пула постоянных времени выполнения разрешаются во время выполнения для определения фактического расположения метода. То же самое относится ко всем другим инструкциям Java Virtual Machine, которые обращаются к экземплярам класса.
Вызов addTwoStatic, класса (static) варианта addTwo, аналогичен, как показано:
int add12and13() {
return addTwoStatic(12, 13);
}
хотя используется другая инструкция вызова метода Java Virtual Machine:
Method int add12and13() 0 bipush 12 2 bipush 13 4 invokestatic #3 // Method Example.addTwoStatic(II)I 7 ireturn
Компиляция вызова метода класса (static) очень похожа на компиляцию вызова метода экземпляра, за исключением того, что этот аргумент не передаётся вызывающим методом. Аргументы метода будут получены, начиная с локальной переменной 0 (§3.6). Инструкция invokestatic всегда используется для вызова методов класса.
Инструкция invokespecial должна использоваться для вызова методов инициализации экземпляров (§3.8). Она также используется при вызове методов в суперклассе (super). Например, заданы классы Near и Far, объявленные как:
class Near {
int it;
int getItNear() {
return it;
}
}
class Far extends Near {
int getItFar() {
return super.getItNear();
}
}
Метод Far.getItFar (который вызывает метод суперкласса) становится:
Method int getItFar() 0 aload_0 1 invokespecial #4 // Method Near.getItNear()I 4 ireturn
Обратите внимание, что методы, вызываемые с помощью инструкции invokespecial, всегда передают this вызываемому методу в качестве первого аргумента. Как обычно, он принимается в локальной переменной 0.
Для вызова целевого объекта дескриптора метода компилятор должен сформировать дескриптор метода, в котором записываются фактические типы аргументов и возвращаемого значения. Компилятор не может выполнять преобразования вызова методов для аргументов; вместо этого он должен помещать их в стек в соответствии с их собственными не преобразованными типами. Компилятор обеспечивает помещение ссылки на объект-дескриптор метода в стек перед аргументами, как обычно. Компилятор генерирует инструкцию invokevirtual, которая ссылается на дескриптор, описывающий типы аргументов и возвращаемого значения. В соответствии со специальной обработкой разрешения методов (§5.4.3.3), инструкция invokevirtual, которая вызывает методы invokeExact или invoke класса java.lang.invoke.MethodHandle, всегда будет связана, при условии, что дескриптор метода имеет синтаксически правильную форму, и типы, указанные в дескрипторе, могут быть разрешены.
Экземпляры класса Java Virtual Machine создаются с помощью инструкции new Java Virtual Machine. Отметим, что на уровне Java Virtual Machine конструктор выглядит как метод с именем <init>, предоставленным компилятором. Этот специально названный метод известен как метод инициализации экземпляра (§2.9). Для данного класса может существовать несколько методов инициализации экземпляра, соответствующих нескольким конструкторам. После создания экземпляра класса и инициализации его переменных экземпляра, включая переменные класса и всех его суперклассов, до их значений по умолчанию, вызывается метод инициализации экземпляра нового экземпляра класса. Например:
Object create() {
return new Object();
}
компилируется в:
Method java.lang.Object create()
0 new #1 // Class java.lang.Object
3 dup
4 invokespecial #4 // Method java.lang.Object.<init>()V
7 areturn
Экземпляры класса передаются и возвращаются (как типы reference) очень похоже на числовые значения, хотя тип reference имеет свой собственный набор инструкций, например:
int i; // An instance variable
MyObj example() {
MyObj o = new MyObj();
return silly(o);
}
MyObj silly(MyObj o) {
if (o != null) {
return o;
} else {
return o;
}
}
становится:
Method MyObj example()
0 new #2 // Class MyObj
3 dup
4 invokespecial #5 // Method MyObj.<init>()V
7 astore_1
8 aload_0
9 aload_1
10 invokevirtual #4 // Method Example.silly(LMyObj;)LMyObj;
13 areturn
Method MyObj silly(MyObj)
0 aload_1
1 ifnull 6
4 aload_1
5 areturn
6 aload_1
7 areturn
Поля экземпляра класса (переменные экземпляра) доступны с помощью инструкций getfield и putfield. Если i является переменной экземпляра типа int, методы setIt и getIt, определённые как:
void setIt(int value) {
i = value;
}
int getIt() {
return i;
}
становятся:
Method void setIt(int) 0 aload_0 1 iload_1 2 putfield #4 // Field Example.i I 5 return Method int getIt() 0 aload_0 1 getfield #4 // Field Example.i I 4 ireturn
Как и операнды инструкций вызова методов, операнды инструкций putfield и getfield (индекс пула постоянных времени выполнения #4) не являются смещениями полей в экземпляре класса. Компилятор генерирует символические ссылки на поля экземпляра, которые хранятся в пуле постоянных времени выполнения. Эти элементы пула постоянных времени выполнения разрешаются во время выполнения, чтобы определить расположение поля в указанном объекте.
Массивы Java Virtual Machine также являются объектами. Массивы создаются и обрабатываются с помощью отдельного набора инструкций. Инструкция newarray используется для создания массива числового типа. Код:
void createBuffer() {
int buffer[];
int bufsz = 100;
int value = 12;
buffer = new int[bufsz];
buffer[10] = value;
value = buffer[11];
}
может быть скомпилирован в:
Method void createBuffer() 0 bipush 100 // Push int constant 100 (bufsz) 2 istore_2 // Store bufsz in local variable 2 3 bipush 12 // Push int constant 12 (value) 5 istore_3 // Store value in local variable 3 6 iload_2 // Push bufsz... 7 newarray int // ...and create new int array of that length 9 astore_1 // Store new array in buffer 10 aload_1 // Push buffer 11 bipush 10 // Push int constant 10 13 iload_3 // Push value 14 iastore // Store value at buffer[10] 15 aload_1 // Push buffer 16 bipush 11 // Push int constant 11 18 iaload // Push value at buffer[11]... 19 istore_3 // ...and store it in value 20 return
Инструкция anewarray используется для создания одномерного массива ссылок на объекты, например:
void createThreadArray() {
Thread threads[];
int count = 10;
threads = new Thread[count];
threads[0] = new Thread();
}
становится:
Method void createThreadArray()
0 bipush 10 // Push int constant 10
2 istore_2 // Initialize count to that
3 iload_2 // Push count, used by anewarray
4 anewarray class #1 // Create new array of class Thread
7 astore_1 // Store new array in threads
8 aload_1 // Push value of threads
9 iconst_0 // Push int constant 0
10 new #1 // Create instance of class Thread
13 dup // Make duplicate reference...
14 invokespecial #5 // ...for Thread's constructor
// Method java.lang.Thread.<init>()V
17 aastore // Store new Thread in array at 0
18 return
Инструкция anewarray также может использоваться для создания первого измерения многомерного массива. В качестве альтернативы, инструкция multianewarray может использоваться для создания нескольких измерений сразу. Например, трёхмерный массив:
int[][][] create3DArray() {
int grid[][][];
grid = new int[10][5][];
return grid;
}
создаётся так:
Method int create3DArray()[][][] 0 bipush 10 // Push int 10 (dimension one) 2 iconst_5 // Push int 5 (dimension two) 3 multianewarray #1 dim #2 // Class [[[I, a three-dimensional // int array; only create the // first two dimensions 7 astore_1 // Store new array... 8 aload_1 // ...then prepare to return it 9 areturn
Первый операнд инструкции multianewarray — индекс пула постоянных времени выполнения для типа класса массива, который должен быть создан. Второй — количество измерений этого типа массива, которые нужно фактически создать. Инструкция multianewarray может использоваться для создания всех измерений типа, как показано в коде для create3DArray. Обратите внимание, что многомерный массив является просто объектом, и поэтому он загружается и возвращается инструкцией aload_1 и areturn соответственно. Подробности о именах классов массивов см. в §4.4.1.
Все массивы имеют связанные длины, которые доступны через инструкцию arraylength.
Компиляция инструкций switch использует инструкции tableswitch и lookupswitch. Инструкция tableswitch используется, когда случаи switch могут быть эффективно представлены в виде индексов в таблице целевых смещений. default целевого смещения switch используется, если значение выражения switch выходит за пределы диапазона допустимых индексов. Например:
int chooseNear(int i) {
switch (i) {
case 0: return 0;
case 1: return 1;
case 2: return 2;
default: return -1;
}
}
компилируется в:
Method int chooseNear(int) 0 iload_1 // Push local variable 1 (argument i) 1 tableswitch 0 to 2: // Valid indices are 0 through 2 0: 28 // If i is 0, continue at 28 1: 30 // If i is 1, continue at 30 2: 32 // If i is 2, continue at 32 default:34 // Otherwise, continue at 34 28 iconst_0 // i was 0; push int constant 0... 29 ireturn // ...and return it 30 iconst_1 // i was 1; push int constant 1... 31 ireturn // ...and return it 32 iconst_2 // i was 2; push int constant 2... 33 ireturn // ...and return it 34 iconst_m1 // otherwise push int constant -1... 35 ireturn // ...and return it
Инструкции tableswitch и lookupswitch виртуальной машины Java работают только с данными типа int. Поскольку операции с типами byte, char или short неявно приводятся к типу int, метод switch, выражение которого оценивается как один из этих типов, компилируется так, как если бы он оценивался как тип int. Если бы метод chooseNear был написан с использованием типа short, те же инструкции виртуальной машины Java были бы сгенерированы, что и при использовании типа int. Другие числовые типы должны быть преобразованы к типу int для использования в switch.
В тех случаях, когда случаи switch являются разреженными, табличное представление инструкции tableswitch становится неэффективным в плане использования памяти. Вместо этого можно использовать инструкцию lookupswitch. Инструкция lookupswitch сопоставляет ключи int (значения меток case) с целевыми смещениями в таблице. При выполнении инструкции lookupswitch значение выражения switch сравнивается с ключами в таблице. Если один из ключей совпадает со значением выражения, выполнение продолжается по соответствующему целевому смещению. Если ни один ключ не совпадает, выполнение продолжается по default смещению. Например, скомпилированный код для:
int chooseFar(int i) {
switch (i) {
case -100: return -1;
case 0: return 0;
case 100: return 1;
default: return -1;
}
}
выглядит точно так же, как код для chooseNear, за исключением инструкции lookupswitch:
Method int chooseFar(int) 0 iload_1 1 lookupswitch 3: -100: 36 0: 38 100: 40 default: 42 36 iconst_m1 37 ireturn 38 iconst_0 39 ireturn 40 iconst_1 41 ireturn 42 iconst_m1 43 ireturn
Виртуальная машина Java предписывает, что таблица инструкции lookupswitch должна быть отсортирована по ключу, чтобы реализации могли использовать поисковые алгоритмы, более эффективные, чем линейный поиск. Тем не менее, инструкция lookupswitch должна искать совпадение среди ключей, а не просто выполнять проверку границ и индексирование в таблицу, как в инструкции tableswitch. Таким образом, инструкция tableswitch вероятно будет более эффективной, чем lookupswitch в ситуациях, где соображения по использованию памяти позволяют сделать выбор.
Виртуальная машина Java имеет обширный набор инструкций, которые манипулируют содержимым стека операндов как неуказанными значениями. Это полезно из-за того, что виртуальная машина Java полагается на умелое манипулирование стеком операндов. Например:
public long nextIndex() {
return index++;
}
private long index = 0;
компилируется в:
Method long nextIndex() 0 aload_0 // Push this 1 dup // Make a copy of it 2 getfield #4 // One of the copies of this is consumed // pushing long field index, // above the original this 5 dup2_x1 // The long on top of the operand stack is // inserted into the operand stack below the // original this 6 lconst_1 // Push long constant 1 7 ladd // The index value is incremented... 8 putfield #4 // ...and the result stored in the field 11 lreturn // The original value of index is on top of // the operand stack, ready to be returned
Обратите внимание, что виртуальная машина Java никогда не разрешает инструкциям по манипулированию стеком операндов изменять или разбивать отдельные значения в стеке операндов.
Исключения выбрасываются из программ с помощью ключевого слова throw. Его компиляция проста:
void cantBeZero(int i) throws TestExc {
if (i == 0) {
throw new TestExc();
}
}
преобразуется в:
Method void cantBeZero(int)
0 iload_1 // Push argument 1 (i)
1 ifne 12 // If i==0, allocate instance and throw
4 new #1 // Create instance of TestExc
7 dup // One reference goes to its constructor
8 invokespecial #7 // Method TestExc.<init>()V
11 athrow // Second reference is thrown
12 return // Never get here if we threw TestExc
Компиляция конструкций try-catch проста. Например:
void catchOne() {
try {
tryItOut();
} catch (TestExc e) {
handleExc(e);
}
}
компилируется как:
Method void catchOne() 0 aload_0 // Beginning of try block 1 invokevirtual #6 // Method Example.tryItOut()V 4 return // End of try block; normal return 5 astore_1 // Store thrown value in local var 1 6 aload_0 // Push this 7 aload_1 // Push thrown value 8 invokevirtual #5 // Invoke handler method: // Example.handleExc(LTestExc;)V 11 return // Return after handling TestExc Exception table: From To Target Type 0 4 5 Class TestExc
Более детально, блок try компилируется так же, как если бы try не существовало:
Method void catchOne() 0 aload_0 // Beginning of try block 1 invokevirtual #6 // Method Example.tryItOut()V 4 return // End of try block; normal return
Если во время выполнения блока try исключение не выбрасывается, он ведет себя так, как если бы try отсутствовал: вызывается tryItOut и catchOne возвращает значение.
За блоком try следует код виртуальной машины Java, реализующий единственное положение catch:
5 astore_1 // Store thrown value in local var 1 6 aload_0 // Push this 7 aload_1 // Push thrown value 8 invokevirtual #5 // Invoke handler method: // Example.handleExc(LTestExc;)V 11 return // Return after handling TestExc Exception table: From To Target Type 0 4 5 Class TestExc
Вызов handleExc, содержимое положения catch, также компилируется как обычный вызов метода. Однако, наличие положения catch заставляет компилятор сгенерировать запись в таблице исключений (§2.10, §4.7.3). Таблица исключений для метода catchOne содержит одну запись, соответствующую одному аргументу (экземпляр класса TestExc), который может обработать положение catch в catchOne. Если во время выполнения инструкций между индексами 0 и 4 в catchOne будет брошено значение, являющееся экземпляром класса TestExc, управление передается коду виртуальной машины Java по индексу 5, который реализует блок положения catch. Если брошенное значение не является экземпляром класса TestExc, положение catch в catchOne не может его обработать. Вместо этого значение будет переброшено вызывающей стороне catchOne.
В блоке try может быть несколько положений catch:
void catchTwo() {
try {
tryItOut();
} catch (TestExc1 e) {
handleExc(e);
} catch (TestExc2 e) {
handleExc(e);
}
}
Несколько положений catch в данном утверждении try компилируются путем простого добавления кода виртуальной машины Java для каждого положения catch друг за другом и добавления записей в таблицу исключений, как показано:
Method void catchTwo() 0 aload_0 // Begin try block 1 invokevirtual #5 // Method Example.tryItOut()V 4 return // End of try block; normal return 5 astore_1 // Beginning of handler for TestExc1; // Store thrown value in local var 1 6 aload_0 // Push this 7 aload_1 // Push thrown value 8 invokevirtual #7 // Invoke handler method: // Example.handleExc(LTestExc1;)V 11 return // Return after handling TestExc1 12 astore_1 // Beginning of handler for TestExc2; // Store thrown value in local var 1 13 aload_0 // Push this 14 aload_1 // Push thrown value 15 invokevirtual #7 // Invoke handler method: // Example.handleExc(LTestExc2;)V 18 return // Return after handling TestExc2 Exception table: From To Target Type 0 4 5 Class TestExc1 0 4 12 Class TestExc2
Если во время выполнения положения try (между индексами 0 и 4) будет брошено значение, соответствующее параметру одного или нескольких положений catch (значение является экземпляром одного или нескольких параметров), будет выбрано первое (внутреннее) такое положение catch. Управление передаётся коду виртуальной машины Java для блока этого положения catch. Если брошенное значение не соответствует ни одному из параметров положений catch в catchTwo, виртуальная машина Java перебрасывает это значение без вызова кода в любом положении catch в catchTwo.
Вложенные утверждения try-catch компилируются очень похоже на утверждение try с несколькими положениями catch:
void nestedCatch() {
try {
try {
tryItOut();
} catch (TestExc1 e) {
handleExc1(e);
}
} catch (TestExc2 e) {
handleExc2(e);
}
}
преобразуется в:
Method void nestedCatch() 0 aload_0 // Begin try block 1 invokevirtual #8 // Method Example.tryItOut()V 4 return // End of try block; normal return 5 astore_1 // Beginning of handler for TestExc1; // Store thrown value in local var 1 6 aload_0 // Push this 7 aload_1 // Push thrown value 8 invokevirtual #7 // Invoke handler method: // Example.handleExc1(LTestExc1;)V 11 return // Return after handling TestExc1 12 astore_1 // Beginning of handler for TestExc2; // Store thrown value in local var 1 13 aload_0 // Push this 14 aload_1 // Push thrown value 15 invokevirtual #6 // Invoke handler method: // Example.handleExc2(LTestExc2;)V 18 return // Return after handling TestExc2 Exception table: From To Target Type 0 4 5 Class TestExc1 0 12 12 Class TestExc2
Вложенность положений catch представлена только в таблице исключений. Виртуальная машина Java не накладывает ограничений на вложенность или порядок записей в таблице исключений (§2.10). Однако, поскольку конструкции try-catch структурированы, компилятор всегда может упорядочить записи в таблице обработчиков исключений таким образом, чтобы для любого брошенного исключения и любого значения счётчика программы в этом методе, первый обработчик исключений, соответствующий брошенному исключению, соответствовал самому внутреннему совпадающему положению catch.
Например, если вызов tryItOut (по индексу 1) бросил экземпляр TestExc1, он будет обработан положением catch, которое вызовет handleExc1. Это так даже если исключение возникает в границах внешнего положения catch (обрабатывающего TestExc2) и даже если это внешнее положение catch могло бы в ином случае обработать брошенное значение.
Обратите внимание, что диапазон положения catch включительно для начала и исключительно для конца («from» и «to») (§4.7.3). Таким образом, запись в таблице исключений для положения catch, обрабатывающего TestExc1, не охватывает инструкцию return по смещению 4. Однако, запись в таблице исключений для положения catch, обрабатывающего TestExc2, охватывает инструкцию return по смещению 11. Инструкции возврата внутри вложенных положений catch включаются в диапазон инструкций, охватываемый вложенными положениями catch.
(Данный раздел предполагает, что компилятор генерирует файлы class с номером версии 50.0 или ниже, чтобы можно было использовать инструкцию jsr. См. также §4.10.2.5.)
Компиляция оператора try-finally аналогична компиляции оператора try-catch. Перед передачей управления за пределы оператора try, будь то нормальная передача или прерывистая из-за возникновения исключения, необходимо выполнить блок finally. Для простого примера:
void tryFinally() {
try {
tryItOut();
} finally {
wrapItUp();
}
}
сгенерированный код:
Method void tryFinally() 0 aload_0 // Beginning of try block 1 invokevirtual #6 // Method Example.tryItOut()V 4 jsr 14 // Call finally block 7 return // End of try block 8 astore_1 // Beginning of handler for any throw 9 jsr 14 // Call finally block 12 aload_1 // Push thrown value 13 athrow // ...and rethrow value to the invoker 14 astore_2 // Beginning of finally block 15 aload_0 // Push this 16 invokevirtual #5 // Method Example.wrapItUp()V 19 ret 2 // Return from finally block Exception table: From To Target Type 0 4 8 any
Существует четыре способа выхода из оператора try: падение до конца блока, возврат, выполнение оператора break или continue, или возбуждение исключения. Если tryItOut возвращается без возбуждения исключения, управление передается блоку finally с использованием инструкции jsr. Инструкция jsr 14 в индексе 4 выполняет "вызов подпрограммы" коду блока finally в индексе 14 (блок finally компилируется как вложенная подпрограмма). После завершения блока finally инструкция ret 2 возвращает управление инструкции, следующей за инструкцией jsr в индексе 4.
Более подробно, вызов подпрограммы работает следующим образом: Инструкция jsr помещает адрес следующей инструкции (return в индексе 7) в стек операндов перед переходом. Инструкция astore_2, являющаяся целью перехода, сохраняет адрес из стека операндов в локальную переменную 2. Выполняется код блока finally (в данном случае инструкции aload_0 и invokevirtual). Если выполнение этого кода завершается нормально, инструкция ret извлекает адрес из локальной переменной 2 и возобновляет выполнение по этому адресу. Выполняется инструкция return, и tryFinally возвращается нормально.
Оператор try с блоком finally компилируется со специальным обработчиком исключений, который может обрабатывать любые исключения, возбужденные внутри оператора try. Если tryItOut возбуждает исключение, таблица исключений для tryFinally ищет подходящий обработчик исключений. Специальный обработчик найден, и выполнение продолжается в индексе 8. Инструкция astore_1 в индексе 8 сохраняет брошенное значение в локальную переменную 1. Следующая инструкция jsr выполняет вызов подпрограммы кода блока finally. Если этот код возвращается нормально, инструкция aload_1 в индексе 12 помещает брошенное значение обратно в стек операндов, а следующая инструкция athrow повторно возбуждает значение.
Компиляция оператора try с блоком catch и блоком finally сложнее:
void tryCatchFinally() {
try {
tryItOut();
} catch (TestExc e) {
handleExc(e);
} finally {
wrapItUp();
}
}
становится:
Method void tryCatchFinally() 0 aload_0 // Beginning of try block 1 invokevirtual #4 // Method Example.tryItOut()V 4 goto 16 // Jump to finally block 7 astore_3 // Beginning of handler for TestExc; // Store thrown value in local var 3 8 aload_0 // Push this 9 aload_3 // Push thrown value 10 invokevirtual #6 // Invoke handler method: // Example.handleExc(LTestExc;)V 13 goto 16 // This goto is unnecessary, but was // generated by javac in JDK 1.0.2 16 jsr 26 // Call finally block 19 return // Return after handling TestExc 20 astore_1 // Beginning of handler for exceptions // other than TestExc, or exceptions // thrown while handling TestExc 21 jsr 26 // Call finally block 24 aload_1 // Push thrown value... 25 athrow // ...and rethrow value to the invoker 26 astore_2 // Beginning of finally block 27 aload_0 // Push this 28 invokevirtual #5 // Method Example.wrapItUp()V 31 ret 2 // Return from finally block Exception table: From To Target Type 0 4 7 Class TestExc 0 16 20 any
Если оператор try завершается нормально, инструкция goto в индексе 4 переходит к вызову подпрограммы для блока finally в индексе 16. Блок finally в индексе 26 выполняется, управление возвращается к инструкции return в индексе 19, и tryCatchFinally возвращается нормально.
Если tryItOut возбуждает экземпляр TestExc, выбирается первый (внутренний) подходящий обработчик исключений в таблице исключений для обработки исключения. Код этого обработчика исключений, начинающийся с индекса 7, передает брошенное значение handleExc, а при возврате выполняется тот же вызов подпрограммы для блока finally в индексе 26, что и в нормальном случае. Если handleExc не возбуждает исключение, tryCatchFinally возвращается нормально.
Если tryItOut возбуждает значение, которое не является экземпляром TestExc, или если сам handleExc возбуждает исключение, условие обрабатывается второй записью в таблице исключений, которая обрабатывает любое брошенное значение между индексами 0 и 16. Этот обработчик исключений передает управление индексу 20, где брошенное значение сначала сохраняется в локальной переменной 1. Вызывается код блока finally в индексе 26 как подпрограмма. Если он возвращается, брошенное значение извлекается из локальной переменной 1 и повторно возбуждается с помощью инструкции athrow. Если во время выполнения блока finally возбуждается новое значение, блок finally прерывается, и tryCatchFinally возвращается неожиданно, возбуждая новое значение для своего вызывающего метода.
Синхронизация в Java Virtual Machine реализуется через вход и выход в монитор, явным образом (с помощью инструкций monitorenter и monitorexit) или неявно (инструкциями вызова и возврата метода).
Для кода, написанного на языке программирования Java, наиболее распространённой формой синхронизации является метод synchronized. Метод synchronized обычно не реализуется с помощью monitorenter и monitorexit. Вместо этого он просто обозначается во временном константном пуле флагом ACC_SYNCHRONIZED, который проверяется инструкциями вызова метода (§2.11.10).
Инструкции monitorenter и monitorexit позволяют компилировать операторы synchronized. Например:
void onlyMe(Foo f) {
synchronized(f) {
doSomething();
}
}
компилируется в:
Method void onlyMe(Foo) 0 aload_1 // Push f 1 dup // Duplicate it on the stack 2 astore_2 // Store duplicate in local variable 2 3 monitorenter // Enter the monitor associated with f 4 aload_0 // Holding the monitor, pass this and... 5 invokevirtual #5 // ...call Example.doSomething()V 8 aload_2 // Push local variable 2 (f) 9 monitorexit // Exit the monitor associated with f 10 goto 18 // Complete the method normally 13 astore_3 // In case of any throw, end up here 14 aload_2 // Push local variable 2 (f) 15 monitorexit // Be sure to exit the monitor! 16 aload_3 // Push thrown value... 17 athrow // ...and rethrow value to the invoker 18 return // Return in the normal case Exception table: From To Target Type 4 10 13 any 13 16 13 any
Компилятор гарантирует, что при завершении любого вызова метода для каждой инструкции monitorenter, выполненной с момента вызова метода, будет выполнена инструкция monitorexit. Это происходит как при нормальном завершении вызова метода (§2.6.4), так и при прерывистом завершении (§2.6.5). Для обеспечения правильного сопряжения инструкций monitorenter и monitorexit при прерывистом завершении вызова метода компилятор генерирует обработчики исключений (§2.10), которые будут соответствовать любому исключению и выполнение соответствующего кода будет выполнять необходимые инструкции monitorexit.
Представление аннотаций в файлах class описано в §4.7.16-§4.7.22. Эти разделы подробно описывают, как представлять аннотации в объявлениях классов, интерфейсов, полей, методов, параметров методов и параметров типа, а также аннотации типов, используемых в этих объявлениях. Аннотации в объявлениях пакетов требуют дополнительных правил, приведенных ниже.
При обнаружении компилятором объявления аннотированного пакета, которое должно быть доступно во время выполнения, он генерирует файл class со следующими свойствами:
-
Файл
classпредставляет собой интерфейс, то есть флагиACC_INTERFACEиACC_ABSTRACTструктурыClassFileустановлены (§4.1). -
Если номер версии файла
classменьше 50.0, то флагACC_SYNTHETICсброшен; если номер версии файлаclassравен или больше 50.0, то флагACC_SYNTHETICустановлен. -
Интерфейс имеет доступ к пакету (JLS §6.6.1).
-
Имя интерфейса — внутренняя форма (§4.2.1) для
package-name.package-info. -
У интерфейса нет суперинтерфейсов.
-
Единственными членами интерфейса являются те, которые подразумеваются в Спецификации языка Java, издание Java SE 11 (JLS §9.2).
-
Аннотации объявления пакета хранятся в качестве атрибутов
RuntimeVisibleAnnotationsиRuntimeInvisibleAnnotationsв таблицеattributesструктурыClassFile.
Единица компиляции, содержащая объявление модуля (JLS §7.7), компилируется в файл class, содержащий атрибут Module.
По соглашению, имя единицы компиляции, содержащей объявление модуля, — module-info.java, что отражает соглашение package-info.java для единицы компиляции, содержащей только объявление пакета. Следовательно, по соглашению, имя для скомпилированной формы объявления модуля — module-info.class.
Флаг в элементе access_flags структуры ClassFile, ACC_MODULE (0x8000), указывает, что данный файл class объявляет модуль. ACC_MODULE играет аналогичную роль, что ACC_ANNOTATION (0x2000) и ACC_ENUM (0x4000) при обозначении этого файла class как «не обычный класс». ACC_MODULE не описывает доступность класса или интерфейса.
Атрибут Module явно описывает зависимости модуля; неявные директивы requires на уровне ClassFile отсутствуют. Если элемент requires_count равен нулю, платформа Java SE не выводит существование таблицы requires или какой-либо записи в ней. java.base является единственным модулем, в котором ноль requires_count допустим, так как это исходный модуль. Для любого другого модуля атрибут Module должен содержать таблицу requires длиной не менее единицы, поскольку каждый другой модуль зависит от java.base. Если единица компиляции содержит объявление модуля (кроме java.base), которое явно не указывает свою зависимость от java.base, компилятор должен вывести запись для java.base в таблице requires и пометить ее как ACC_MANDATED, чтобы указать, что она была объявлена неявно.
Для инкапсуляции атрибут Module явно описывает пакеты, экспортируемые и открытые обычным модулем; неявные директивы exports или opens на уровне ClassFile для обычного модуля отсутствуют. Если элемент exports_count или элемент opens_count равен нулю, платформа Java SE не выводит существование таблицы exports или opens, или какие-либо записи в них. С другой стороны, для открытого модуля атрибут Module подразумевает пакеты, открытые модулем. Все пакеты открытого модуля открыты для всех других модулей, даже если элемент opens_count равен нулю.
Атрибут Module явно описывает потребление и предоставление услуг модулем; неявные директивы uses или provides на уровне ClassFile отсутствуют.
© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.