为什么 Java 编译器复制 finally 阻塞?
Why does the Java Compiler copy finally blocks?
当使用简单的 try/finally
块编译以下代码时,Java 编译器生成以下输出(在 ASM 字节码查看器中查看):
代码:
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
在中间添加 catch
块时,我注意到编译器复制了 finally
块 3 次(不再发布字节码)。这似乎是 class 文件中 space 的浪费。复制似乎也不限于最大指令数(类似于内联的工作方式),因为当我向 System.out.println
.[=24 添加更多调用时它甚至复制了 finally
块=]
然而,我的自定义编译器使用不同方法编译相同代码的结果在执行时完全相同,但通过使用 GOTO
指令需要更少 space:
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...
内联 Finally 块
您提出的问题已在 http://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on-the-jvm/(回程机网络存档 link)
中进行了部分分析
post 将展示一个有趣的示例以及诸如 (quote):
等信息
finally blocks are implemented by inlining the finally code at all possible exits from the try or associated catch blocks, wrapping the whole thing in essentially a “catch(Throwable)” block that rethrows the exception when it finishes, and then adjusting the exception table such that the catch clauses skip over the inlined finally statements. Huh? (Small caveat: prior to the 1.6 compilers, apparently, finally statements used sub-routines instead of full-on code inlining. But we’re only concerned with 1.6 at this point, so that’s what this applies to).
JSR 指令和 Inlined Finally
虽然我还没有从官方文档或来源中找到明确的意见,但对于为什么使用内联存在不同意见。
有以下3种解释:
没有offer优势-比较麻烦:
有些人认为使用 finally 内联是因为 JSR/RET 没有提供主要优势,例如来自 What Java compilers use the jsr instruction, and what for?
的引述
The JSR/RET mechanism was originally used to implement finally blocks. However, they decided that the code size savings weren't worth the extra complexity and it got gradually phased out.
使用堆栈映射表进行验证的问题:
@jeffrey-bosboom 在评论中提出了另一种可能的解释,我在下面引用了他的话:
javac used to use jsr (jump subroutine) to only write finally code once, but there were some problems related to the new verification using stack map tables. I assume they went back to cloning the code just because it was the easiest thing to do.
必须维护子例程脏位:
问题 What Java compilers use the jsr instruction, and what for? 的评论中有趣的交流指出 JSR 和子例程 "added extra complexity from having to maintain a stack of dirty bits for the local variables"。
下方交流:
@paj28: Would the jsr have posed such difficulties if it could only
call declared "subroutines", each of which could only be entered at
the start, would only be callable from one other subroutine, and could
only exit via ret or abrupt completion (return or throw)? Duplicating
code in finally blocks seems really ugly, especially since
finally-related cleanup may often invoke nested try blocks. –
supercat Jan 28 '14 at 23:18
@supercat, Most of that is already true. Subroutines can only be
entered from the start, can only return from one place, and can only
be called from within a single subroutine. The complexity comes from
the fact that you have to maintain a stack of dirty bits for the local
variables and when returning, you have to do a three-way merge. –
Antimony Jan 28 '14 at 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,在第 37 行添加了一个 finally 块,第 49 行用于未选中 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
看起来原来的 finally 块实现类似于您的提议,但是自从 Java 1.4.2 javac 开始内联 finally 块,来自 Hamilton & Danicic 的“An Evaluation of Current Java Bytecode Decompilers”[2009]:
Many of the old decompilers expect the use of
subroutines for try-finally blocks but javac 1.4.2+ generates
inline code instead.
2006 年的 blog post 讨论了这个问题:
The code in lines 5-12 is identical to the code in lines 19-26, which actually translates to the count++ line. The finally block is clearly copied.
当使用简单的 try/finally
块编译以下代码时,Java 编译器生成以下输出(在 ASM 字节码查看器中查看):
代码:
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
在中间添加 catch
块时,我注意到编译器复制了 finally
块 3 次(不再发布字节码)。这似乎是 class 文件中 space 的浪费。复制似乎也不限于最大指令数(类似于内联的工作方式),因为当我向 System.out.println
.[=24 添加更多调用时它甚至复制了 finally
块=]
然而,我的自定义编译器使用不同方法编译相同代码的结果在执行时完全相同,但通过使用 GOTO
指令需要更少 space:
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...
内联 Finally 块
您提出的问题已在 http://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on-the-jvm/(回程机网络存档 link)
中进行了部分分析post 将展示一个有趣的示例以及诸如 (quote):
等信息finally blocks are implemented by inlining the finally code at all possible exits from the try or associated catch blocks, wrapping the whole thing in essentially a “catch(Throwable)” block that rethrows the exception when it finishes, and then adjusting the exception table such that the catch clauses skip over the inlined finally statements. Huh? (Small caveat: prior to the 1.6 compilers, apparently, finally statements used sub-routines instead of full-on code inlining. But we’re only concerned with 1.6 at this point, so that’s what this applies to).
JSR 指令和 Inlined Finally
虽然我还没有从官方文档或来源中找到明确的意见,但对于为什么使用内联存在不同意见。
有以下3种解释:
没有offer优势-比较麻烦:
有些人认为使用 finally 内联是因为 JSR/RET 没有提供主要优势,例如来自 What Java compilers use the jsr instruction, and what for?
的引述The JSR/RET mechanism was originally used to implement finally blocks. However, they decided that the code size savings weren't worth the extra complexity and it got gradually phased out.
使用堆栈映射表进行验证的问题:
@jeffrey-bosboom 在评论中提出了另一种可能的解释,我在下面引用了他的话:
javac used to use jsr (jump subroutine) to only write finally code once, but there were some problems related to the new verification using stack map tables. I assume they went back to cloning the code just because it was the easiest thing to do.
必须维护子例程脏位:
问题 What Java compilers use the jsr instruction, and what for? 的评论中有趣的交流指出 JSR 和子例程 "added extra complexity from having to maintain a stack of dirty bits for the local variables"。
下方交流:
@paj28: Would the jsr have posed such difficulties if it could only call declared "subroutines", each of which could only be entered at the start, would only be callable from one other subroutine, and could only exit via ret or abrupt completion (return or throw)? Duplicating code in finally blocks seems really ugly, especially since finally-related cleanup may often invoke nested try blocks. – supercat Jan 28 '14 at 23:18
@supercat, Most of that is already true. Subroutines can only be entered from the start, can only return from one place, and can only be called from within a single subroutine. The complexity comes from the fact that you have to maintain a stack of dirty bits for the local variables and when returning, you have to do a three-way merge. – Antimony Jan 28 '14 at 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,在第 37 行添加了一个 finally 块,第 49 行用于未选中 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
看起来原来的 finally 块实现类似于您的提议,但是自从 Java 1.4.2 javac 开始内联 finally 块,来自 Hamilton & Danicic 的“An Evaluation of Current Java Bytecode Decompilers”[2009]:
Many of the old decompilers expect the use of subroutines for try-finally blocks but javac 1.4.2+ generates inline code instead.
2006 年的 blog post 讨论了这个问题:
The code in lines 5-12 is identical to the code in lines 19-26, which actually translates to the count++ line. The finally block is clearly copied.