Почему компилятор Java наконец-то копирует блоки?
При компиляции следующего кода с простым try/finally
Блок, компилятор Java производит выходные данные ниже (просмотр в ASM Bytecode Viewer):
Код:
try
{
System.out.println("Attempting to divide by zero...");
System.out.println(1 / 0);
}
finally
{
System.out.println("Finally...");
}
Bytecode:
TRYCATCHBLOCK L0 L1 L1
L0
LINENUMBER 10 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Attempting to divide by zero..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L2
LINENUMBER 11 L2
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ICONST_1
ICONST_0
IDIV
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L3
LINENUMBER 12 L3
GOTO L4
L1
LINENUMBER 14 L1
FRAME SAME1 java/lang/Throwable
ASTORE 1
L5
LINENUMBER 15 L5
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Finally..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L6
LINENUMBER 16 L6
ALOAD 1
ATHROW
L4
LINENUMBER 15 L4
FRAME SAME
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Finally..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L7
LINENUMBER 17 L7
RETURN
L8
LOCALVARIABLE args [Ljava/lang/String; L0 L8 0
MAXSTACK = 3
MAXLOCALS = 2
При добавлении catch
блок между, я заметил, что компилятор скопировал finally
блокировать 3 раза (больше не публиковать байт-код). Это кажется пустой тратой пространства в файле классов. Копирование также не ограничивается максимальным количеством инструкций (аналогично тому, как работает встраивание), поскольку оно даже дублирует finally
заблокировать, когда я добавил больше звонков System.out.println
,
Тем не менее, результат моего собственного компилятора, который использует другой подход к компиляции того же кода, работает точно так же, когда выполняется, но требует меньше места при использовании GOTO
инструкция:
public static main([Ljava/lang/String;)V
// parameter args
TRYCATCHBLOCK L0 L1 L1
L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Attempting to divide by zero..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ICONST_1
ICONST_0
IDIV
INVOKEVIRTUAL java/io/PrintStream.println (I)V
GOTO L2
L1
FRAME SAME1 java/lang/Throwable
POP
L2
FRAME SAME
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Finally..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L3
RETURN
LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
MAXSTACK = 3
MAXLOCALS = 1
Почему компилятор Java (или компилятор Eclipse) копирует байт-код finally
блокировать несколько раз, даже используя athrow
отбрасывать исключения, когда та же семантика может быть достигнута с помощью goto
? Это часть процесса оптимизации, или мой компилятор делает это неправильно?
(Выход в обоих случаях...)
Attempting to divide by zero...
Finally...
2 ответа
В конце концов блоки
Ваш вопрос был частично проанализирован по адресу: http://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on-the-jvm/
Пост покажет интересный пример, а также информацию, такую как (цитата):
Блоки finally реализуются путем встраивания кода finally во все возможные выходы из блоков try или связанных блоков catch, оборачивая все это по существу в блок "catch(Throwable)", который перебрасывает исключение после его завершения, и затем корректируя таблицу исключений таким образом что предложения catch пропускают встроенные операторы finally. А? (Небольшое предостережение: до компилятора 1.6, по-видимому, операторы finally использовали подпрограммы вместо полной вставки кода. Но на данном этапе нас интересует только 1.6, так что это то, к чему это относится).
Инструкция JSR и Inlined Наконец
Существуют разные мнения относительно того, почему используется встраивание, хотя я еще не нашел однозначного из официального документа или источника.
Есть следующие 3 объяснения:
Нет преимуществ предложения - больше проблем:
Некоторые считают, что в конце концов используется in-lining, потому что JSR/RET не предлагал существенных преимуществ, таких как цитата из того, что компиляторы Java используют инструкцию jsr, и для чего?
Механизм JSR/RET изначально использовался для реализации блоков finally. Тем не менее, они решили, что экономия размера кода не стоила дополнительной сложности, и она постепенно прекратилась.
Проблемы с проверкой с использованием таблиц стековых карт:
Другое возможное объяснение было предложено в комментариях @jeffrey-bosboom, которого я цитирую ниже:
javac использовал jsr (подпрограмму перехода), чтобы написать код finally только один раз, но были некоторые проблемы, связанные с новой проверкой с использованием таблиц стековых карт. Я предполагаю, что они вернулись к клонированию кода только потому, что это было проще всего сделать.
Необходимость поддерживать грязные биты подпрограммы:
Интересный обмен в комментариях вопроса. Какие компиляторы Java используют инструкцию jsr и для чего? указывает, что JSR и подпрограммы "добавили дополнительной сложности из-за необходимости поддерживать стек грязных битов для локальных переменных".
Ниже биржи:
@ paj28: если бы jsr создавал такие трудности, он мог бы вызывать только объявленные "подпрограммы", каждая из которых могла быть введена только в начале, была бы вызвана только из одной другой подпрограммы и могла выйти только через ret или внезапное завершение (вернуть или бросить)? Дублирование кода в блоках finally кажется очень уродливым, тем более что связанная с окончанием очистка может часто вызывать вложенные блоки try. - суперкат 28 января '14 в 23:18
@supercat, большинство из этого уже правда. Подпрограммы могут быть введены только с самого начала, могут возвращаться только из одного места и могут вызываться только из одной подпрограммы. Сложность заключается в том, что вам нужно поддерживать стек грязных битов для локальных переменных, а при возврате вы должны выполнить трехстороннее слияние. - Сурьма 28 января '14 в 23:40
Компилируя это:
public static void main(String... args){
try
{
System.out.println("Attempting to divide by zero...");
System.out.println(1 / 0);
}catch(Exception e){
System.out.println("Exception!");
}
finally
{
System.out.println("Finally...");
}
}
И, глядя на результат javap -v, блок finally просто добавляется в конец каждого раздела, который обрабатывает исключение (при добавлении catch добавляется блок finally в строке 37, а блок 49 - для непроверенного java.lang. Ошибки):
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=3, locals=3, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Attempting to divide by zero...
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iconst_1
12: iconst_0
13: idiv
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #6 // String Finally...
22: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: goto 59
28: astore_1
29: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
32: ldc #8 // String Exception!
34: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
40: ldc #6 // String Finally...
42: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
45: goto 59
48: astore_2
49: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
52: ldc #6 // String Finally...
54: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
57: aload_2
58: athrow
59: return
Exception table:
from to target type
0 17 28 Class java/lang/Exception
0 17 48 any
28 37 48 any
Похоже, что оригинальная реализация блоков finally напоминает то, что вы предлагаете, но с тех пор, как Java 1.4.2 javac начал вставлять блоки finally, из " Оценки текущих декомпиляторов байт-кода Java"[2009] Hamilton & Danicic:
Многие старые декомпиляторы ожидают использования подпрограмм для блоков try-finally, но вместо этого javac 1.4.2+ генерирует встроенный код.
Сообщение в блоге от 2006 года, в котором обсуждается это:
Код в строках 5-12 идентичен коду в строках 19-26, который фактически переводится в строку count++. Блок finally четко скопирован.