22

次のコードを単純な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...");
}

バイトコード:

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

間にブロックを追加すると、コンパイラーがブロックを3catch回コピーしたことに気付きました(バイトコードを再投稿していません)。これは、クラス ファイル内のスペースの無駄のようです。への呼び出しをさらに追加したときにブロックを複製したため、コピーも最大命令数に制限されていないようです(インライン化の仕組みと同様)。finallyfinallySystem.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ブロックのバイトコードを複数回コピーし、例外を再スローするために使用するのはなぜですか? これは最適化プロセスの一部ですか、それともコンパイラが間違っているのでしょうか?athrowgoto


(両方の場合の出力は...)

Attempting to divide by zero...
Finally...
4

2 に答える 2

13

finally ブロックのインライン化

あなたの質問はhttp://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on-the-jvm/で部分的に分析されています(wayback machine web archive link)

この投稿では、興味深い例と (引用) などの情報が表示されます。

finally ブロックは、try または関連する catch ブロックからのすべての可能な出口で finally コードをインライン化することによって実装され、本質的にすべてを、終了時に例外を再スローする「catch(Throwable)」ブロックにラップし、次にそのような例外テーブルを調整します。 catch 句がインライン化された finally ステートメントをスキップすること。は?(小さな注意点: 1.6 コンパイラより前では、finally ステートメントは完全なコードのインライン化ではなくサブルーチンを使用していたようです。しかし、現時点では 1.6 のみに関心があるため、これが適用されます)。


JSR 命令と Inlined finally

インライン化が使用される理由についてはさまざまな意見がありますが、公式文書や情報源から決定的なものをまだ見つけていません.

以下の3つの説明があります。

オファーの利点なし - より多くのトラブル:

最終的にインライン化が使用されるのは、JSR/RET が主要な利点を提供しなかったためだと考える人もいます。

JSR/RET メカニズムは、もともとは finally ブロックを実装するために使用されていました。しかし、彼らは、コード サイズの節約は複雑さを増す価値がないと判断し、徐々に段階的に廃止しました。

スタック マップ テーブルを使用した検証に関する問題:

以下に引用する@jeffrey-bosboomによるコメントで、別の可能な説明が提案されています。

javac は jsr (ジャンプ サブルーチン) を使用して finally コードを 1 回だけ記述していましたが、スタック マップ テーブルを使用した新しい検証に関連するいくつかの問題がありました。私は、それが最も簡単な方法だったという理由だけで、コードのクローン作成に戻ったと思います。

サブルーチンのダーティ ビットを維持する必要がある:

質問のコメントでの興味深い交換は、どの Java コンパイラーが jsr 命令を使用し、何のために使用しますか? JSR とサブルーチンは、「ローカル変数のダーティ ビットのスタックを維持する必要があるため、複雑さが増した」ことを指摘しています。

取引所の下:

@paj28: 宣言された「サブルーチン」のみを呼び出すことができ、それぞれが開始時にのみ入ることができ、他のサブルーチンからのみ呼び出すことができ、ret または突然の完了 (返すか投げるか)?finally ブロックでコードを複製することは、特に、finally 関連のクリーンアップがネストされた try ブロックを頻繁に呼び出す可能性があるため、非常に見苦しく見えます。— スーパーキャット

@supercat、そのほとんどはすでに真実です。サブルーチンは、最初からのみ入ることができ、1 つの場所からのみ戻ることができ、1 つのサブルーチン内からのみ呼び出すことができます。複雑さは、ローカル変数のダーティ ビットのスタックを維持する必要があり、戻るときに 3 方向マージを実行する必要があるという事実から生じます。— アンチモン

于 2015-03-15T16:03:42.063 に答える
3

これをコンパイルする:

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 を追加すると、37 行目の finally ブロックが追加され、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

元の最終ブロックの実装はあなたが提案しているものに似ているように見えますが、Java 1.4.2 javac が最終ブロックのインライン展開を開始したため、Hamilton & Danicic の「現在の Java バイトコード逆コンパイラの評価」[2009] から:

古い逆コンパイラの多くは、try-finally ブロックにサブルーチンを使用することを想定していますが、javac 1.4.2+ は代わりにインライン コードを生成します。

これについて説明している 2006 年のブログ投稿:

5 ~ 12 行目のコードは 19 ~ 26 行目のコードと同じで、実際には count++ 行に変換されます。finally ブロックは明確にコピーされています。

于 2015-03-15T16:07:33.827 に答える