Глава 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. Аннотации
Виртуальная машина Java разработана для поддержки языка программирования Java. Программное обеспечение JDK Oracle содержит компилятор из исходного кода, написанного на языке программирования Java, в набор инструкций виртуальной машины Java, а также среду выполнения, которая реализует саму виртуальную машину Java. Понимание того, как один компилятор использует виртуальную машину Java, полезно как для потенциального разработчика компилятора, так и для тех, кто пытается понять саму виртуальную машину Java. Нумерованные разделы в этой главе не являются нормативными.
Обратите внимание, что термин «компилятор» иногда используется применительно к переводчику из набора инструкций виртуальной машины Java в набор инструкций конкретного процессора. Примером такого переводчика является генератор кода в реальном времени (JIT), который генерирует инструкции, специфичные для платформы, только после загрузки кода виртуальной машины Java. В этой главе не рассматриваются вопросы, связанные с генерацией кода, а только те, которые связаны с компиляцией исходного кода, написанного на языке программирования Java, в инструкции виртуальной машины Java.
Эта глава в основном состоит из примеров исходного кода вместе с аннотированными списками кода виртуальной машины Java, которые компилятор javac в выпуске JDK Oracle 1.0.2 генерирует для примеров. Код виртуальной машины Java написан на неофициальном «языке ассемблера виртуальной машины», выводимом утилитой Oracle's javap, распространяемой с выпуском JDK. Вы можете использовать javap для генерации дополнительных примеров скомпилированных методов.
Формат примеров должен быть понятен любому, кто читал код ассемблера. Каждая инструкция имеет вид:
<индекс> <код_операции> [ <операнд1> [ <операнд2>... ]] [<комментарий>]
Значение <индекс> — индекс кода операции инструкции в массиве, который содержит байты кода виртуальной машины для этого метода. В качестве альтернативы, <индекс> можно рассматривать как смещение в байтах от начала метода. <код_операции> — мнемоника кода операции инструкции, а ноль или более <операндN> — операнды инструкции. Необязательный <комментарий> указан в синтаксисе комментария в конце строки:
8 bipush 100 // Push int constant 100
Некоторая информация в комментариях выводится утилитой javap; остальное предоставлено авторами. <индекс>, предваряющий каждую инструкцию, может использоваться в качестве целевого адреса управляющей инструкции. Например, инструкция 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 хранится как локальная переменная виртуальной машины Java 1. Поскольку большинство инструкций виртуальной машины 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.2).
Например, сравнение значений типа int в утверждении for примера spin может быть реализовано с помощью одной команды if_icmplt; однако в системе команд виртуальной машины Java нет одной команды, которая выполняет условный переход по значениям типа double. Таким образом, dspin должен реализовать сравнение значений типа double с помощью команды dcmpg, за которой следует команда iflt.
Виртуальная машина Java предоставляет наибольшую поддержку данных типа int. Это отчасти в ожидании эффективных реализаций стеков операндов и массивов локальных переменных виртуальной машины Java. Это также мотивировано частотой использования данных типа int в типичных программах. Другие целочисленные типы имеют меньшую непосредственную поддержку. Например, нет byte, char, или short версий команд сохранения, загрузки или сложения. Вот пример 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 до допустимых диапазонов.
Типы long и с плавающей точкой имеют промежуточный уровень поддержки в виртуальной машине 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 помещает -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 текущего экземпляра, this, в стек операндов. Затем передаются аргументы вызова метода, значения 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) и при вызове методов private. Например, заданы классы Near и Far, объявленные как:
class Near {
int it;
public int getItNear() {
return getIt();
}
private int getIt() {
return it;
}
}
class Far extends Near {
int getItFar() {
return super.getItNear();
}
}
метод Near.getItNear (который вызывает метод private) становится:
Method int getItNear() 0 aload_0 1 invokespecial #5 // Method Near.getIt()I 4 ireturn
Метод Far.getItFar (который вызывает метод суперкласса) становится:
Method int getItFar() 0 aload_0 1 invokespecial #4 // Method Near.getItNear()I 4 ireturn
Обратите внимание, что методы, вызываемые с помощью инструкции invokespecial, всегда передают this вызываемому методу в качестве первого аргумента. Как обычно, он принимается в локальной переменной 0.
Для вызова цели обработчика методов компилятор должен сформировать дескриптор метода, который записывает фактические типы аргументов и возвращаемого значения. Компилятор не может выполнять преобразования вызова методов над аргументами; вместо этого он должен поместить их в стек в соответствии с их собственными необработанными типами. Компилятор организует помещение reference объекта обработчика методов в стек перед аргументами, как обычно. Компилятор генерирует инструкцию 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. Управление передается коду виртуальной машины для блока этого обработчика 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 помещает адрес следующей инструкции (возврат по индексу 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 реализуется с помощью входа и выхода из монитора, либо явно (с помощью инструкций 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
Компилятор гарантирует, что при завершении любого вызова метода будет выполнена инструкция monitorexit для каждой инструкции monitorenter, выполненной с момента вызова метода. Это происходит независимо от того, завершается ли вызов метода нормально (§2.6.4) или внезапно (§2.6.5). Для обеспечения правильной парности инструкций monitorenter и monitorexit при внезапном завершении вызова метода компилятор генерирует обработчики исключений (§2.10), которые будут соответствовать любому исключению, и связанный с ними код выполнит необходимые инструкции monitorexit.
Представление аннотаций в файлах class описано в §4.7.16 и §4.7.17, которые подробно описывают, как представлять аннотации на типах, полях и методах в формате файлов class. Аннотации пакетов требуют дополнительных правил, приведенных здесь.
Когда компилятор сталкивается с объявлением аннотированного пакета, который должен быть доступен во время выполнения, он генерирует файл class, представляющий собой интерфейс, имя которого является внутренней формой (§4.2.1) package-name.package-info. Интерфейс имеет доступ по умолчанию (JLS §6.6.1) и не имеет суперинтерфейсов. Флаги ACC_INTERFACE и ACC_ABSTRACT структуры ClassFile (§4.1) установлены (Таблица 4.1). Если номер версии сгенерированного файла class меньше 50.0, флаг ACC_SYNTHETIC не установлен; если номер версии файла класса 50.0 или выше, флаг ACC_SYNTHETIC установлен. Единственными членами интерфейса являются те, что подразумеваются в Спецификации языка Java, Java SE 7 Edition (JLS §9.2).
Аннотации пакета хранятся в атрибутах RuntimeVisibleAnnotations (§4.7.16) и RuntimeInvisibleAnnotations (§4.7.17) структуры ClassFile (§4.1) этого интерфейса.
© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.