Подтвердить что ты не робот

Почему компилятор 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...
4b9b3361

Ответ 1

Блоки Inlining Наконец

Вопрос, который вы задали, был частично проанализирован на 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: если бы это могло сделать вызов объявил "подпрограммы", каждая из которых может быть введена только в начало будет вызываться только из одной подпрограммы и может выход только через ret или внезапное завершение (возврат или бросок)? Дублирование код в блоках finally кажется очень уродливым, тем более окончательная очистка может часто вызывать вложенные блоки try. - суперкат 28 января '14 в 23:18

@supercat, большинство из этого уже правда. Подпрограммы могут быть только введен с самого начала, может вернуться только из одного места, и может только вызываться из одной подпрограммы. Сложность исходит от тот факт, что вы должны поддерживать стек грязных битов для локального переменных и при возврате, вы должны сделать трехстороннее слияние. - Сурьма 28 января '14 в 23:40

Ответ 2

Компиляция:

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 для unchecked java.lang.Errors):

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

Похоже, что оригинал, наконец, блокирует реализацию, похожую на то, что вы предлагаете, но так как Java 1.4.2 javac начал встраивать, наконец, блоки, из "" Оценка текущей Java "Byteecode Decompilers" [2009] Гамильтона и Данича:

Многие из старых декомпиляторов ждут использования подпрограммы для блоков try-finally, но javac 1.4.2+ генерирует inline code.

A сообщение в блоге от 2006 года, где обсуждаются следующие вопросы:

Код в строках 5-12 идентичен коду в строках 19-26, который фактически переводится в строку count ++. Блок finally явно скопирован.