Spec-Zone.ru › Java Virtual Machine Specification 17

Глава 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.

3.1. Формат примеров

Эта глава в основном состоит из примеров исходного кода вместе с аннотированными списками кода виртуальной машины Java, который компилятор javac в релизе JDK 1.0.2 Oracle генерирует для примеров. Код виртуальной машины Java написан на неофициальном «языке ассемблера виртуальной машины», выводимом утилитой javap Oracle, распространяемой с релизом JDK. Вы можете использовать javap для генерации дополнительных примеров скомпилированных методов.

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

<index> <opcode> [ <operand1> [ <operand2>... ]] [<comment>]

<index> — это индекс операционного кода инструкции в массиве, содержащем байты кода виртуальной машины Java для этого метода. В качестве альтернативы, <index> можно рассматривать как смещение байтов от начала метода. <opcode> — это мнемоника для операционного кода инструкции, а ноль или более <operandN> — это операнды инструкции. Необязательный <comment> приводится в синтаксисе комментариев в конце строки:

8   bipush 100     // Push int constant 100

Часть информации в комментариях генерируется javap; остальное — авторами. <index>, предшествующее каждой инструкции, может использоваться в качестве цели инструкции передачи управления. Например, инструкция goto 8 передает управление инструкции с индексом 8. Обратите внимание, что фактические операнды инструкций передачи управления виртуальной машины Java — это смещения относительно адресов операционных кодов этих инструкций; эти операнды отображаются javap (и показаны в этой главе) как более легко читаемые смещения в их методах.

Мы предваряем операнд, представляющий индекс пула постоянных времени выполнения, знаком решетки и следуем за инструкцией комментарием, идентифицирующим элемент пула постоянных времени выполнения, на который делается ссылка, как в:

10  ldc #1         // Push float constant 100.0

или:

9   invokevirtual #4    // Method Example.addTwo(II)I

В целях этой главы мы не беспокоимся о детализации таких аспектов, как размер операндов.

3.2. Использование констант, локальных переменных и управляющих конструкций

Код 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, у которых отсутствует только полный набор инструкций условного перехода.

3.3. Арифметика

Виртуальная машина 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.4. Доступ к пулу констант во время выполнения

Многие числовые константы, а также объекты, поля и методы, доступны через пул констант во время выполнения текущего класса. Доступ к объектам рассматривается позже (§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...

3.5. Дополнительные примеры управления

Компиляция 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.

3.6. Прием аргументов

Если методу-экземпляру передано 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.

3.7. Вызов методов

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

3.8. Работа с экземплярами классов

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

3.9. Массивы

Массивы виртуальной машины 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.

3.10. Компиляция переключателей

Компиляция инструкций 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 при возможности выбора.

3.11. Операции со стеком операндов

Виртуальная машина 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, манипулирующие стеком операндов, никогда не изменяют и не разбивают отдельные значения в стеке операндов.

3.12. Генерация и обработка исключений

Исключения генерируются в программах с использованием ключевого слова 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.

3.13. Компиляция блока finally

(Этот раздел предполагает, что компилятор генерирует файлы с номером версии 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 возвращает значение с ошибкой, выбрасывая новое значение своему вызывающему объекту.

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

Синхронизация в 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.

3.15. Аннотации

Представление аннотаций в файлах 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 (JLS §9.2).

  • Аннотации на объявлении пакета хранятся в качестве атрибутов RuntimeVisibleAnnotations и RuntimeInvisibleAnnotations в таблице attributes структуры ClassFile.

3.16. Модули

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

Spec-Zone.ru

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