Понимание локальной позиции var в байт-коде JVM, наконец

У меня проблемы с пониманием позиционирования переменных в байт-коде ASMified Java. У меня есть следующий Java-код:

public class TryCatch {
    public static void main(String[] args) {
        String test1 = null;
        try {
            String test2 ="try-inside-begin";
            System.out.println("try-outside-begin");
            try {
                System.out.println(test2);
                System.out.println(test1.length());
                System.out.println("try-inside-end");
            } catch (NullPointerException e) {
                test2 = "catch-inside: " + e.getMessage();
                throw new Exception(test2, e);
            }
            System.out.println("try-outside-end");
        } catch (Exception e) {
            System.out.println("catch-outside: " + e.getMessage());
        } finally {
            System.out.println("finally");
        }
    }
}

Что становится следующим байт-кодом для main:

  TRYCATCHBLOCK L0 L1 L2 java/lang/NullPointerException
  TRYCATCHBLOCK L3 L4 L5 java/lang/Exception
  TRYCATCHBLOCK L3 L4 L6 null
  TRYCATCHBLOCK L5 L7 L6 null
  TRYCATCHBLOCK L6 L8 L6 null
 L9
  LINENUMBER 5 L9
  ACONST_NULL
  ASTORE 1
 L3
  LINENUMBER 7 L3
  LDC "try-inside-begin"
  ASTORE 2
 L10
  LINENUMBER 8 L10
  GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
  LDC "try-outside-begin"
  INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
 L0
  LINENUMBER 10 L0
  GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
  ALOAD 2
  INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
 L11
  LINENUMBER 11 L11
  GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
  ALOAD 1
  INVOKEVIRTUAL java/lang/String.length ()I
  INVOKEVIRTUAL java/io/PrintStream.println (I)V
 L12
  LINENUMBER 12 L12
  GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
  LDC "try-inside-end"
  INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
 L1
  LINENUMBER 16 L1
  GOTO L13
 L2
  LINENUMBER 13 L2
 FRAME FULL [[Ljava/lang/String; java/lang/String java/lang/String] [java/lang/NullPointerException]
  ASTORE 3
 L14
  LINENUMBER 14 L14
  NEW java/lang/StringBuilder
  DUP
  INVOKESPECIAL java/lang/StringBuilder.<init> ()V
  LDC "catch-inside: "
  INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
  ALOAD 3
  INVOKEVIRTUAL java/lang/NullPointerException.getMessage ()Ljava/lang/String;
  INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
  INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
  ASTORE 2
 L15
  LINENUMBER 15 L15
  NEW java/lang/Exception
  DUP
  ALOAD 2
  ALOAD 3
  INVOKESPECIAL java/lang/Exception.<init> (Ljava/lang/String;Ljava/lang/Throwable;)V
  ATHROW
 L13
  LINENUMBER 17 L13
 FRAME SAME
  GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
  LDC "try-outside-end"
  INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
 L4
  LINENUMBER 21 L4
  GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
  LDC "finally"
  INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
 L16
  LINENUMBER 22 L16
  GOTO L17
 L5
  LINENUMBER 18 L5
 FRAME FULL [[Ljava/lang/String; java/lang/String] [java/lang/Exception]
  ASTORE 2
 L18
  LINENUMBER 19 L18
  GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
  NEW java/lang/StringBuilder
  DUP
  INVOKESPECIAL java/lang/StringBuilder.<init> ()V
  LDC "catch-outside: "
  INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
  ALOAD 2
  INVOKEVIRTUAL java/lang/Exception.getMessage ()Ljava/lang/String;
  INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
  INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
  INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
 L7
  LINENUMBER 21 L7
  GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
  LDC "finally"
  INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
 L19
  LINENUMBER 22 L19
  GOTO L17
 L6
  LINENUMBER 21 L6
 FRAME SAME1 java/lang/Throwable
  ASTORE 4
 L8
  GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
  LDC "finally"
  INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
  ALOAD 4
  ATHROW
 L17
  LINENUMBER 23 L17
 FRAME SAME
  RETURN
  MAXSTACK = 4
  MAXLOCALS = 5

Обратите внимание, как близко к низу есть ASTORE 4/ALOAD 4. Почему 4, а не 3? Поскольку кадр SAME1 является «теми же локальными переменными, что и предыдущий кадр, и с одним значением в стеке», а предыдущий кадр имеет только две локальные переменные (ссылка: FRAME FULL [[Ljava/lang/String; java/lang/String] [java/lang/Exception]).

Я прочитал спецификацию а мне вот тоже непонятно почему не 3.


person Chad Retz    schedule 17.11.2016    source источник
comment
Я подозреваю, что это может быть 3, за исключением того, что предыдущее использование 3 означает, что проще заставить его использовать 4, а не повторно использовать 3.   -  person Peter Lawrey    schedule 17.11.2016
comment
Эта часть спецификации не очень полезна, так как описывает устаревший механизм, основанный на jsr/ret. Как правило, компилятор может использовать столько устаревших дополнительных локальных переменных, сколько пожелает, и как описано здесь и здесь, javac сгенерированный код обработки исключений далек от оптимального.   -  person Holger    schedule 17.11.2016
comment
@Holger - я полагаю, это означает, что если я создаю интерпретатор байт-кода, я не могу полагаться на то, что локальные переменные будут запрашиваться по порядку? Например. текущий фрейм в 3 локальных переменных может запрашивать 5-й слот и игнорировать 4. :-(   -  person Chad Retz    schedule 17.11.2016
comment
Точно. Поскольку метод объявляет максимальное число, это не должно вызвать проблем, поскольку вы можете предварительно выделить таблицу/массив/и т. д. Обеспечение того, чтобы ни одна переменная не читалась перед записью, является задачей верификатора.   -  person Holger    schedule 17.11.2016


Ответы (1)


Фрейм стека описывает состояние локальных переменных и стека операндов в той точке, где он появляется. Более поздние инструкции могут, конечно, изменить вещи, как обычно. Как вы правильно определили, кадр стека на L6 говорит, что есть две локальные переменные, когда поток управления достигает L6. Следующая инструкция сохраняет данные в слот 4, что вполне допустимо.

Это может помочь понять назначение карты стека. Первоначально карты стека вообще не было, и верификатор использовал вывод для вычисления локальных переменных в каждой точке метода. При встрече с потоком управления он будет объединять значения в этой точке и выполнять итерации до сходимости.

К сожалению, это было медленно, поэтому, пытаясь ускорить процесс, Oracle добавил карты стека. По сути, это предварительно вычисляет результаты проверки в любой точке, где соединяется поток управления. Таким образом, верификатор может выполнить один линейный проход по коду, потому что поток управления не изменяет результаты. Когда верификатор сталкивается с потоком управления, он проверяет, соответствует ли текущее состояние кадру стека, объявленному в цели перехода, и, если нет, выдает ошибку. В разделах линейного кода, очевидно, нет необходимости включать кадры стека, поскольку верификатор может просто делать то же самое, что и раньше.

Кадры стека не предназначены для отладки, они предназначены для ускорения проверки, поэтому они содержат минимум информации, необходимой для проверки. Если бы компилятор гипотетически вставлял фрейм стека в каждую инструкцию, то фрейм стека после astore 4, конечно же, отображал бы новую переменную в 4-м слоте.

Что касается того, почему он использовал слот 4, когда он мог бы использовать слот 3, это просто прихоть компилятора. Возможно, это упростило реализацию javac, но это только предположения.

person Antimony    schedule 17.11.2016
comment
Последний абзац — это то, что я искал. Почему они выбрали слот 4 вместо 3. Спасибо. - person Chad Retz; 22.11.2016