Почему компилятор 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 четко скопирован.

Другие вопросы по тегам