Глава 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. Обратите внимание, что фактическими операндами инструкций передачи управления виртуальной машины Java являются смещения от адресов операционных кодов этих инструкций; эти операнды отображаются javap (и показаны в этой главе) в виде более легко читаемых смещений в их методы.
Мы предваряем операнд, представляющий индекс постоянного пула времени выполнения, знаком решётки и сопровождаем инструкцию комментарием, определяющим элемент постоянного пула времени выполнения, на который ссылаются, как в:
10 ldc #1 // Pushfloatconstant100.0
или:
9 invokevirtual #4 // Method Example.addTwo(II)I
В целях этой главы мы не будем подробно рассматривать такие детали, как размеры операндов.
Код Java Virtual Machine обладает набором общих характеристик, накладываемых конструкцией и использованием типов Java Virtual Machine. В первом примере мы сталкиваемся со многими из них, и мы рассмотрим их подробно.
Метод 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 Virtual Machine ориентирована на стек, при этом большинство операций берут один или несколько операндов со стека операндов текущей рамки Java Virtual Machine или помещают результаты обратно на стек операндов. Каждый раз при вызове метода создается новая рамка, а вместе с ней — новый стек операндов и набор локальных переменных для использования этим методом (§2.6). В любой момент вычисления, таким образом, вероятно, есть много рамок и столько же стеков операндов на поток управления, соответствующих многоуровневым вызовам методов. Только стек операндов в текущей рамке активен.
Набор инструкций Java Virtual Machine различает типы операндов, используя отдельные байткоды для операций над различными типами данных. Метод spin работает только со значениями типа int. Инструкции в его скомпилированном коде, выбранные для работы с типизированными данными (iconst_0, istore_1, iinc, iload_1, if_icmplt), все специализированы для типа int.
Две константы в spin, 0 и 100, помещаются на стек операндов с помощью двух различных инструкций. 0 помещается с помощью инструкции iconst_0, одной из семейства инструкций iconst_<i>. 100 помещается с помощью инструкции bipush, которая извлекает значение, которое она помещает, как непосредственный операнд.
Java Virtual Machine часто использует вероятность определенных операндов (int константы -1, 0, 1, 2, 3, 4 и 5 в случае инструкций iconst_<i>), сделав эти операнды неявными в коде операции. Поскольку инструкция iconst_0 знает, что она будет помещать значение int 0, iconst_0 не нужно хранить операнд, чтобы указать ей какое значение поместить, а также не нужно извлекать или декодировать операнд. Компиляция помещения 0 как bipush 0 была бы правильной, но сделала бы скомпилированный код для spin на один байт длиннее. Простая виртуальная машина также потратила бы дополнительное время на извлечение и декодирование явного операнда каждый раз по кругу цикла. Использование неявных операндов делает скомпилированный код более компактным и эффективным.
Значение int i в spin хранится как локальная переменная Java Virtual Machine 1. Поскольку большинство инструкций Java Virtual Machine работают со значениями, извлеченными из стека операндов, а не непосредственно с локальными переменными, инструкции, которые передают значения между локальными переменными и стеком операндов, являются распространенными в коде, скомпилированном для Java Virtual Machine. Эти операции также имеют специальную поддержку в наборе инструкций. В spin значения передаются в локальные переменные и из них с помощью инструкций istore_1 и iload_1, каждая из которых неявно работает с локальной переменной 1. Инструкция istore_1 извлекает значение int со стека операндов и сохраняет его в локальной переменной 1. Инструкция iload_1 помещает значение в локальную переменную 1 на стек операндов.
Использование (и повторное использование) локальных переменных является обязанностью автора компилятора. Специализированные инструкции загрузки и сохранения должны поощрять автора компилятора повторно использовать локальные переменные настолько, насколько это возможно. Полученный код быстрее, компактнее и занимает меньше места в рамке.
Определенные очень частые операции над локальными переменными специально поддерживаются Java Virtual Machine. Инструкция 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 Virtual Machine составляет 1 байт, что приводит к очень компактности скомпилированного кода. Однако 1-байтовые коды операций также означают, что набор инструкций Java Virtual Machine должен оставаться небольшим. В качестве компромисса Java Virtual Machine не предоставляет равной поддержки для всех типов данных: он не полностью ортогонален (Таблица 2.11.1-A).
Например, сравнение значений типа int в инструкции for примера spin может быть реализовано с помощью одной инструкции if_icmplt; однако нет единой инструкции в наборе инструкций Java Virtual Machine, которая выполняет условный переход на значения типа double. Таким образом, dspin должен реализовать сравнение значений типа double с помощью инструкции dcmpg, за которой следует инструкция iflt.
Java Virtual Machine предоставляет наибольшую непосредственную поддержку данных типа int. Это частично из-за ожидаемого повышения эффективности реализации стеков операндов и массивов локальных переменных Java Virtual Machine. Это также мотивировано частотой данных типа int в типичных программах. Другие целочисленные типы имеют меньшую непосредственную поддержку. Например, нет byte, char или short версий инструкций сохранения, загрузки или сложения. Вот пример spin, написанный с использованием short:
void sspin() {
short i;
for (i = 0; i < 100; i++) {
; // Loop body is empty
}
}
Он должен быть скомпилирован для Java Virtual Machine, как показано ниже, с использованием инструкций, работающих с другим типом, скорее всего, 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 Virtual Machine не является особенно проблематичным, поскольку значения этих типов неявно преобразуются в int (byte и short расширяются со знаком до int, char расширяется с нулем). Операции над данными byte, char и short могут выполняться с использованием инструкций int. Единственной дополнительной стоимостью является обрезка значений операций int до допустимого диапазона.
Типы long и с плавающей точкой имеют промежуточный уровень поддержки в Java Virtual Machine, у которых отсутствует только полный набор инструкций условного перехода.
Виртуальная машина 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 помещает -1 в стек операндов, а инструкция ifge не разветвляется. Если d больше, чем 100.0, или равно NaN, инструкция dcmpg помещает 1 в стек операндов, и инструкция ifge разветвляется. Если d равно 100.0, инструкция dcmpg помещает 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 помещает значение в стек операндов, которое заставляет 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 предоставляет отдельные инструкции возврата для многих своих числовых и reference типов данных, а также инструкцию return для методов без возвращаемого значения. Такой же набор инструкций возврата используется для всех разновидностей вызовов методов.
Операнд инструкции invokevirtual (в примере, индекс пула постоянных времени выполнения #4) не является смещением метода в экземпляре класса. Компилятор не знает внутреннего расположения экземпляра класса. Вместо этого он генерирует символические ссылки на методы экземпляра, которые хранятся в пуле постоянных времени выполнения. Эти элементы пула постоянных времени выполнения разрешаются во время выполнения для определения фактического местоположения метода. То же самое справедливо для всех других инструкций виртуальной машины Java, которые обращаются к экземплярам классов.
Вызов метода addTwoStatic, варианта класса (static) метода addTwo, аналогичен, как показано:
int add12and13() {
return addTwoStatic(12, 13);
}
хотя используется другая инструкция вызова метода виртуальной машины Java:
Method int add12and13() 0 bipush 12 2 bipush 13 4 invokestatic #3 // Method Example.addTwoStatic(II)I 7 ireturn
Компиляция вызова метода класса (static) очень похожа на компиляцию вызова метода экземпляра, за исключением того, что this не передаётся вызывающим методом. Таким образом, аргументы метода будут получены, начиная с локальной переменной 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 создаются с помощью инструкции new виртуальной машины Java. На уровне виртуальной машины Java конструктор представлен как метод с именем <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 также являются объектами. Массивы создаются и обрабатываются с помощью отдельного набора инструкций. Инструкция 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 значениями в рамках виртуальной машины Java неявно преобразуются к типу 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 не было: try вызывается, а 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 включает левую границу и не включает правую (§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 24 Edition (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.