Spec-Zone.ru › Java Virtual Machine Specification 8

Глава 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 в набор инструкций конкретного процессора. Одним примером такого переводчика является генератор кода Just-in-time (JIT), который генерирует платформозависимые инструкции только после загрузки кода виртуальной машины Java. В этой главе не рассматриваются проблемы, связанные с генерацией кода, а только проблемы, связанные с компиляцией исходного кода, написанного на языке программирования Java, в инструкции виртуальной машины Java.

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

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

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

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

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

8   bipush 100     // Push int constant 100

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

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

10  ldc #1         // Push float constant 100.0

или:

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

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

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

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

Метод spin просто выполняет пустой цикл for 100 раз:


void spin() {
    int i;
    for (i = 0; i < 100; i++) {
        ;    // Loop body is empty
    }
}

Компилятор может скомпилировать spin в:

0   iconst_0       // Push int constant 0
1   istore_1       // Store into local variable 1 (i=0)
2   goto 8         // First time through don't increment
5   iinc 1 1       // Increment local variable 1 by 1 (i++)
8   iload_1        // Push local variable 1 (i)
9   bipush 100     // Push int constant 100
11  if_icmplt 5    // Compare and loop if less than (i < 100)
14  return         // Return void when done

Виртуальная машина Java имеет стековый характер, причём большинство операций берут один или несколько операндов со стека операндов текущей рамки виртуальной машины Java или помещают результаты обратно в стек операндов. Каждая вызов метода создает новую рамку, вместе с ней создается новый стек операндов и набор локальных переменных для использования данным методом (§2.6). В любой момент вычисления, таким образом, могут быть много рамок и столько же стеков операндов на каждый поток управления, что соответствует множеству вложенных вызовов методов. Только стек операндов в текущей рамке активен.

Набор инструкций виртуальной машины Java различает типы операндов, используя отдельные байткоды для операций над различными типами данных. Метод spin работает только со значениями типа int. Инструкции в его скомпилированном коде, выбранные для работы с типизированными данными (iconst_0, istore_1, iinc, iload_1, if_icmplt) все специализированы для типа int.

Две константы в spin, 0 и 100, помещаются в стек операндов с помощью двух разных инструкций. 0 помещается с помощью инструкции iconst_0, одной из семейства инструкций iconst_<i>. 100 помещается с помощью инструкции bipush, которая извлекает значение, которое она помещает, как непосредственный операнд.

Виртуальная машина Java часто использует вероятность определенных операндов (int констант -1, 0, 1, 2, 3, 4 и 5 в случае с инструкциями iconst_<i>) сделав эти операнды неявными в коде операции. Так как инструкция iconst_0 знает, что она собирается поместить целое число int 0, iconst_0 не нужно хранить операнд, чтобы сказать ей, какое значение поместить, ни извлекать или декодировать операнд. Скомпилировать помещение 0 как bipush 0 было бы правильно, но сделало бы скомпилированный код для spin на один байт длиннее. Простая виртуальная машина также потратила бы дополнительное время на извлечение и декодирование явного операнда каждый раз при повторении цикла. Использование неявных операндов делает скомпилированный код более компактным и эффективным.

Локальная переменная int i в spin хранится как локальная переменная 1 виртуальной машины Java. Поскольку большинство инструкций виртуальной машины Java оперируют значениями, извлеченными из стека операндов, а не непосредственно локальными переменными, инструкции, которые передают значения между локальными переменными и стеком операндов, являются распространенными в коде, скомпилированном для виртуальной машины Java. Эти операции также имеют специальную поддержку в наборе инструкций. В spin значения передаются в локальные переменные и из них с помощью инструкций istore_1 и iload_1, каждая из которых неявно работает с локальной переменной 1. Инструкция istore_1 извлекает целое число int со стека операндов и сохраняет его в локальной переменной 1. Инструкция iload_1 помещает значение из локальной переменной 1 в стек операндов.

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

Определенные очень частые операции с локальными переменными обрабатываются специально виртуальной машиной Java. Инструкция iinc увеличивает содержимое локальной переменной на значение со знаком размером в один байт. Инструкция iinc в spin увеличивает первую локальную переменную (ее первый операнд) на 1 (ее второй операнд). Инструкция iinc очень удобна при реализации циклических конструкций.

Цикл for в spin выполняется в основном этими инструкциями:

5   iinc 1 1       // Increment local variable 1 by 1 (i++)
8   iload_1        // Push local variable 1 (i)
9   bipush 100     // Push int constant 100
11  if_icmplt 5    // Compare and loop if less than (i < 100)

Инструкция bipush помещает значение 100 в стек операндов в качестве int, а затем инструкция if_icmplt извлекает это значение со стека операндов и сравнивает его с i. Если сравнение успешно (переменная i меньше 100), управление передается в индекс 5 и начинается следующий цикл for. В противном случае управление передается инструкции, следующей за if_icmplt.

Если в примере spin для счетчика цикла был использован тип данных, отличный от int, скомпилированный код обязательно изменится, чтобы отразить разный тип данных. Например, если вместо целого числа int пример spin использует double, как показано:


void dspin() {
    double i;
    for (i = 0.0; i < 100.0; i++) {
        ;    // Loop body is empty
    }
}

скомпилированный код:

Method void dspin()
0   dconst_0       // Push double constant 0.0
1   dstore_1       // Store into local variables 1 and 2
2   goto 9         // First time through don't increment
5   dload_1        // Push local variables 1 and 2 
6   dconst_1       // Push double constant 1.0 
7   dadd           // Add; there is no dinc instruction
8   dstore_1       // Store result in local variables 1 and 2
9   dload_1        // Push local variables 1 and 2 
10  ldc2_w #4      // Push double constant 100.0 
13  dcmpg          // There is no if_dcmplt instruction
14  iflt 5         // Compare and loop if less than (i < 100.0)
17  return         // Return void when done

Инструкции, работающие с типизированными данными, теперь специализированы для типа double. (Инструкция ldc2_w будет обсуждена позже в этой главе.)

Вспомним, что значения double занимают две локальные переменные, хотя к ним обращаются только используя меньший индекс из двух локальных переменных. Это также относится к значениям типа long. Например,


double doubleLocals(double d1, double d2) {
    return d1 + d2;
}

становится

Method double doubleLocals(double,double)
0   dload_1       // First argument in local variables 1 and 2
1   dload_3       // Second argument in local variables 3 and 4
2   dadd
3   dreturn

Обратите внимание, что локальные переменные пар локальных переменных, используемых для хранения значений типа double в doubleLocals, никогда не должны манипулироваться индивидуально.

Размер кода операции виртуальной машины Java в 1 байт приводит к тому, что он очень компактный. Однако 1-байтовые коде операции означают, что набор инструкций виртуальной машины Java должен оставаться небольшим. В качестве компромисса виртуальная машина Java не предоставляет одинаковой поддержки для всех типов данных: она не полностью ортогональна (Таблица 2.11.1-A).

Например, сравнение значений типа int в операторе for примера spin может быть реализовано с помощью одной инструкции if_icmplt; однако, нет одной инструкции в наборе инструкций виртуальной машины Java, которая выполняет условный переход на значениях типа double. Таким образом, dspin должен реализовать сравнение значений типа double с помощью инструкции dcmpg, за которой следует инструкция iflt.

Виртуальная машина Java предоставляет наибольшую прямую поддержку данных типа int. Это частично обусловлено ожиданием эффективных реализаций стеков операндов и массивов локальных переменных виртуальной машины Java. Это также обусловлено частотой данных типа int в типичных программах. Другие целочисленные типы имеют менее прямую поддержку. Например, нет инструкций short, char или byte для операций сохранения, загрузки или сложения. Вот пример spin, написанный с использованием short:


void sspin() {
    short i;
    for (i = 0; i < 100; i++) {
        ;    // Loop body is empty
    }
}

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

Method void sspin()
0   iconst_0
1   istore_1
2   goto 10
5   iload_1        // The short is treated as though an int
6   iconst_1
7   iadd
8   i2s            // Truncate int to short
9   istore_1
10  iload_1
11  bipush 100
13  if_icmplt 5
16  return

Отсутствие прямой поддержки типов byte, char и short в виртуальной машине Java не является значительной проблемой, потому что значения этих типов неявно преобразуются к типу int (byte и short расширяются со знаком до int, char расширяется с нулем). Таким образом, операции с данными byte, char и short могут быть выполнены с помощью инструкций int. Единственная дополнительная стоимость — это усечение значений операций с int до допустимых диапазонов.

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

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 помещает значение int -1 в стек операндов, а инструкция ifge не выполняет перехода. Если d больше 100.0 или является NaN, инструкция dcmpg помещает значение int 1 в стек операндов, а инструкция ifge выполняет переход. Если d равно 100.0, инструкция dcmpg помещает значение int 0 в стек операндов, а инструкция ifge выполняет переход.

Инструкция dcmpl достигает того же результата, если сравнение обратное:


int greaterThan100(double d) {
    if (d > 100.0) {
        return 1;			
    } else {
        return -1;			
    }
}

становится:

Method int greaterThan100(double)
0   dload_1
1   ldc2_w #4      // Push double constant 100.0
4   dcmpl          // Push -1 if d is NaN or d < 100.0;
                   // push 0 if d == 100.0
5   ifle 10        // Branch on 0 or -1
8   iconst_1
9   ireturn
10  iconst_m1
11  ireturn

Еще раз, независимо от того, завершилось ли сравнение неудачей для значения, отличного от NaN, или потому, что ему был передан NaN, инструкция dcmpl помещает значение int в стек операндов, что заставляет инструкцию ifle выполнить переход. Если обе инструкции dcmp не существовали, одному из методов примера пришлось бы выполнить больше работы для обнаружения NaN.

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

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

END_OF_DOCUMENT_MARKER

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 неявно преобразуются к типу 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 не присутствовал:

Method void catchOne()
0   aload_0             // Beginning of try block
1   invokevirtual #6    // Method Example.tryItOut()V
4   return              // End of try block; normal return

Если исключение не выбрасывается во время выполнения блока try, он ведёт себя так, как будто блок try отсутствует: вызывается tryItOut и catchOne возвращает управление.

После блока try следует код виртуальной машины Java, реализующий единственный блок catch:

5   astore_1            // Store thrown value in local var 1
6   aload_0             // Push this
7   aload_1             // Push thrown value
8   invokevirtual #5    // Invoke handler method: 
                        // Example.handleExc(LTestExc;)V
11  return              // Return after handling TestExc
Exception table:
From    To      Target      Type
0       4       5           Class TestExc

Вызов handleExc, содержимое блока catch, также компилируется как обычный вызов метода. Однако наличие блока catch заставляет компилятор генерировать запись в таблице обработки исключений (§2.10, §4.7.3). Таблица обработки исключений для метода catchOne содержит одну запись, соответствующую единственному аргументу (экземпляру класса TestExc), который может обработать блок catch оператора catchOne. Если во время выполнения инструкции между индексами 0 и 4 в методе catchOne выбрасывается значение, являющееся экземпляром класса TestExc, управление передаётся коду виртуальной машины Java по индексу 5, реализующему блок оператора catch. Если выбрасываемое значение не является экземпляром класса TestExc, блок catch оператора catchOne не может обработать его. Вместо этого значение повторно выбрасывается вызывающему методу catchOne.

Оператор try может содержать несколько блоков catch:


void catchTwo() {
    try {
        tryItOut();
    } catch (TestExc1 e) {
        handleExc(e);
    } catch (TestExc2 e) {
        handleExc(e);
    }
}

Несколько блоков catch данного оператора try компилируются путём простого добавления кода виртуальной машины Java для каждого блока catch друг за другом и добавления записей в таблицу обработки исключений, как показано:

Method void catchTwo()
0   aload_0             // Begin try block
1   invokevirtual #5    // Method Example.tryItOut()V
4   return              // End of try block; normal return
5   astore_1            // Beginning of handler for TestExc1;
                        // Store thrown value in local var 1
6   aload_0             // Push this
7   aload_1             // Push thrown value
8   invokevirtual #7    // Invoke handler method:
                        // Example.handleExc(LTestExc1;)V
11  return              // Return after handling TestExc1
12  astore_1            // Beginning of handler for TestExc2;
                        // Store thrown value in local var 1
13  aload_0             // Push this
14  aload_1             // Push thrown value
15  invokevirtual #7    // Invoke handler method:
                        // Example.handleExc(LTestExc2;)V
18  return              // Return after handling TestExc2
Exception table:
From    To      Target      Type
0       4       5           Class TestExc1
0       4       12          Class TestExc2

Если во время выполнения блока try (между индексами 0 и 4) выбрасывается значение, соответствующее параметру одного или нескольких блоков catch (значение является экземпляром одного или нескольких параметров), выбирается первый (внутренний) такой блок catch. Управление передаётся коду виртуальной машины Java для блока этого оператора catch. Если выброшенное значение не соответствует параметру ни одного из блоков catch оператора catchTwo, виртуальная машина Java повторно выбрасывает значение без вызова кода в блоке catch оператора catchTwo.

Вложенные операторы try-catch компилируются очень похоже на оператор try с несколькими блоками catch:


void nestedCatch() {
    try {
        try {
            tryItOut();
        } catch (TestExc1 e) {
            handleExc1(e);
        }
    } catch (TestExc2 e) {
        handleExc2(e);
    }
}

преобразуется в:

Method void nestedCatch()
0   aload_0             // Begin try block
1   invokevirtual #8    // Method Example.tryItOut()V
4   return              // End of try block; normal return
5   astore_1            // Beginning of handler for TestExc1;
                        // Store thrown value in local var 1
6   aload_0             // Push this
7   aload_1             // Push thrown value
8   invokevirtual #7    // Invoke handler method: 
                        // Example.handleExc1(LTestExc1;)V
11  return              // Return after handling TestExc1
12  astore_1            // Beginning of handler for TestExc2;
                        // Store thrown value in local var 1
13  aload_0             // Push this
14  aload_1             // Push thrown value
15  invokevirtual #6    // Invoke handler method:
                        // Example.handleExc2(LTestExc2;)V
18  return              // Return after handling TestExc2
Exception table:
From    To      Target      Type
0       4       5           Class TestExc1
0       12      12          Class TestExc2

Вложенность блоков catch представлена только в таблице обработки исключений. Виртуальная машина Java не накладывает ограничений на вложенность или порядок записей в таблице обработки исключений (§2.10). Однако, так как конструкции try-catch структурированы, компилятор всегда может упорядочить записи в таблице обработки исключений таким образом, чтобы для любого выброшенного исключения и любого значения счётчика команд в этом методе первый обработчик исключений, соответствующий выброшенному исключению, соответствовал внутреннему сопоставляемому блоку catch.

Например, если вызов tryItOut (по индексу 1) бросил экземпляр класса TestExc1, он был бы обработан блоком catch, вызывающим handleExc1. Это происходит даже если исключение возникает внутри границ внешнего блока catch (обрабатывающего TestExc2) и даже если этот внешний блок catch мог бы обработать выброшенное значение.

Обратите внимание, что диапазон блока catch включает "от" и не включает "до" (§4.7.3). Таким образом, запись в таблице обработки исключений для блока catch, обрабатывающего TestExc1, не охватывает инструкцию return по смещению 4. Однако запись в таблице обработки исключений для блока catch, обрабатывающего TestExc2, охватывает инструкцию return по смещению 11. Инструкции возврата внутри вложенных блоков catch включены в диапазон инструкций, охватываемый вложенными блоками catch.

END_OF_DOCUMENT_MARKER

3.13. Компиляция finally

(Этот раздел предполагает, что компилятор генерирует файлы class с номером версии 50.0 или ниже, чтобы можно было использовать инструкцию jsr. См. также §4.10.2.5.)

Компиляция оператора try-finally аналогична компиляции операторов try-catch. Перед передачей управления за пределы оператора try, независимо от того, является ли эта передача нормальной или внезапной (из-за возникшего исключения), необходимо выполнить блок finally. Для простого примера:


void tryFinally() {
    try {
        tryItOut();
    } finally {
        wrapItUp();
    }
}

скомпилированный код выглядит следующим образом:

Method void tryFinally()
0   aload_0             // Beginning of try block
1   invokevirtual #6    // Method Example.tryItOut()V
4   jsr 14              // Call finally block
7   return              // End of try block
8   astore_1            // Beginning of handler for any throw
9   jsr 14              // Call finally block
12  aload_1             // Push thrown value
13  athrow              // ...and rethrow value to the invoker
14  astore_2            // Beginning of finally block
15  aload_0             // Push this
16  invokevirtual #5    // Method Example.wrapItUp()V
19  ret 2               // Return from finally block
Exception table:
From    To      Target      Type
0       4       8           any

Существует четыре способа выхода из оператора try: падение до конца блока, возврат, выполнение оператора break или continue, или поднятие исключения. Если tryItOut возвращается без поднятия исключения, управление передаётся блоку finally с помощью инструкции jsr. Инструкция jsr 14 по индексу 4 выполняет «вызов подпрограммы» кода блока finally по индексу 14 (блок finally компилируется как вложенная подпрограмма). По завершении блока finally инструкция ret 2 возвращает управление инструкции, следующей за инструкцией jsr по индексу 4.

Более подробно, вызов подпрограммы работает следующим образом: Инструкция jsr помещает адрес следующей инструкции (return по индексу 7) на стек операндов перед переходом. Инструкция astore_2, которая является целевым адресом перехода, сохраняет адрес из стека операндов в локальную переменную 2. Выполняется код блока finally (в данном случае инструкции aload_0 и invokevirtual). Предполагая, что выполнение этого кода завершается нормально, инструкция ret извлекает адрес из локальной переменной 2 и возобновляет выполнение по этому адресу. Выполняется инструкция return, и tryFinally возвращается нормально.

Оператор try с блоком finally компилируется со специальным обработчиком исключений, способным обработать любое исключение, возникшее внутри оператора try. Если tryItOut выбрасывает исключение, в таблице исключений для tryFinally ищется соответствующий обработчик исключений. Специальный обработчик находится, и выполнение продолжается по индексу 8. Инструкция astore_1 по индексу 8 сохраняет брошенное значение в локальную переменную 1. Следующая инструкция jsr выполняет вызов подпрограммы для блока finally. Предполагая, что этот код возвращается нормально, инструкция aload_1 по индексу 12 возвращает брошенное значение обратно на стек операндов, а последующая инструкция athrow выбрасывает значение.

Компиляция оператора try с блоком catch и блоком finally является более сложной:


void tryCatchFinally() {
    try {
        tryItOut();
    } catch (TestExc e) {
        handleExc(e);
    } finally {
        wrapItUp();
    }
}

преобразуется в:

Method void tryCatchFinally()
0   aload_0             // Beginning of try block
1   invokevirtual #4    // Method Example.tryItOut()V
4   goto 16             // Jump to finally block
7   astore_3            // Beginning of handler for TestExc;
                        // Store thrown value in local var 3
8   aload_0             // Push this
9   aload_3             // Push thrown value
10  invokevirtual #6    // Invoke handler method:
                        // Example.handleExc(LTestExc;)V
13  goto 16             // This goto is unnecessary, but was
                        // generated by javac in JDK 1.0.2
16  jsr 26              // Call finally block
19  return              // Return after handling TestExc
20  astore_1            // Beginning of handler for exceptions
                        // other than TestExc, or exceptions
                        // thrown while handling TestExc
21  jsr 26              // Call finally block
24  aload_1             // Push thrown value...
25  athrow              // ...and rethrow value to the invoker
26  astore_2            // Beginning of finally block
27  aload_0             // Push this
28  invokevirtual #5    // Method Example.wrapItUp()V
31  ret 2               // Return from finally block
Exception table:
From    To      Target      Type
0       4       7           Class TestExc
0       16      20          any

Если оператор try выполняется нормально, инструкция goto по индексу 4 переходит к вызову подпрограммы блока finally по индексу 16. Блок finally по индексу 26 выполняется, управление возвращается к инструкции return по индексу 19, и tryCatchFinally возвращается нормально.

Если tryItOut выбрасывает экземпляр TestExc, выбирается первый (внутренний) подходящий обработчик исключений в таблице исключений для обработки исключения. Код этого обработчика исключений, начинающийся с индекса 7, передает брошенное значение в handleExc, а при возврате выполняет тот же вызов подпрограммы для блока finally по индексу 26, как и в нормальном случае. Если handleExc не выбрасывает исключение, tryCatchFinally возвращается нормально.

Если tryItOut выбрасывает значение, которое не является экземпляром TestExc, или если handleExc само выбрасывает исключение, условие обрабатывается второй записью в таблице исключений, которая обрабатывает любое значение, сброшенное между индексами 0 и 16. Этот обработчик исключений передает управление в индекс 20, где сброшенное значение сначала сохраняется в локальной переменной 1. Вызывается код блока finally по индексу 26 как подпрограмма. Если он возвращается, брошенное значение извлекается из локальной переменной 1 и выбрасывается заново с помощью инструкции athrow. Если новое значение выбрасывается во время выполнения блока finally, блок finally прерывается, и tryCatchFinally возвращается внезапно, бросив новое значение своему вызывающему объекту.

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, издание Java SE 8 (JLS §9.2).

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

© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.

Spec-Zone.ru

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