当使用简单的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
在添加中间的catch
块时,我注意到编译器将finally
块复制了3次(不再发布字节码)。这似乎是在浪费类文件中的空间。复制似乎也不限于指令的最大数量(类似于内联的工作原理),因为当我添加更多对System.out.println
的调用时,它甚至复制了finally
块。
然而,我的一个自定义编译器使用不同的方法编译相同的代码,其结果在执行时完全相同,但通过使用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
既然使用goto
可以实现相同的语义,为什么Java Compiler (或Eclipse Compiler)要多次复制finally
块的字节码,甚至使用athrow
重新抛出异常?这是优化过程的一部分,还是我的编译器做错了?
(两种情况下的输出都是...)
Attempting to divide by zero...
Finally...
发布于 2015-03-16 00:03:42
内联Finally块
你问的问题已经在http://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on-the-jvm/ (wayback machine web archive链接)上进行了部分分析
这篇文章将展示一个有趣的例子,以及(引用)这样的信息:
Throwable finally块的实现方法是:在try或关联的catch块的所有可能出口内联
代码,将整个代码包装在一个“catch (Throwable)”块中,该块在异常结束时重新抛出异常,然后调整异常表,以便catch子句跳过内联的finally语句。哈?(小提示:在1.6编译器之前,显然,finally语句使用子例程而不是完整的代码内联。但在这一点上,我们只关心1.6,所以这就是适用的)。
JSR指令和Inlined
关于为什么使用内联有不同的观点,尽管我还没有从官方文件或来源中找到一个明确的观点。
有以下3种解释:
没有提供优势-更多麻烦:
有些人认为,最终使用内联是因为JSR/RET没有提供主要的优势,比如引用What Java compilers use the jsr instruction, and what for?
最初用于实现finally块。然而,他们认为节省的代码大小不值得额外的复杂性,它逐渐被淘汰。
使用堆栈映射表进行验证的问题:
@jeffrey-bosboom在评论中提出了另一种可能的解释,我在下面引用他的话:
javac过去使用jsr (跳转子例程)只写一次最终代码,但是在使用堆栈映射表的新验证中出现了一些问题。我猜想他们又回到了克隆代码,因为这是最容易做的事情。
必须维护子例程脏位的:
在问题What Java compilers use the jsr instruction, and what for?的注释中有一个有趣的交换,指出JSR和子例程“由于必须为局部变量维护一堆脏位而增加了额外的复杂性”。
以下是交换:
@paj28:如果jsr只能调用声明的"subroutines",每个子例程只能在开始时进入,只能从另一个子例程调用,并且只能通过ret或突然完成(返回或抛出)退出,那么jsr会带来这样的困难吗?在finally块中复制代码看起来真的很难看,特别是因为与finally相关的清理通常会调用嵌套的try块。- supercat Jan 28 '14在23:18
@supercat,大部分都已经是真的了。子例程只能从头开始进入,只能从一个位置返回,并且只能从单个子例程中调用。复杂性来自于这样一个事实,即你必须为局部变量维护一堆脏位,当返回时,你必须进行三向合并。- Jan 28 '14 at 23:40
发布于 2015-03-16 00:07:34
编译以下代码:
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的结果,finally块被简单地附加到管理异常的每个部分的末尾(添加-v,在第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块实现与您提出的类似,但是自从Java1.4.2javac开始内联finally块以来,来自Hamilton &Danicic的"An Evaluation of Current Java Bytecode Decompilers"2009:
许多旧的反编译器都希望使用子例程来执行
-finally代码块,但是javac 1.4.2+生成的是内联代码。
2006年的一篇blog post讨论了这一点:
第5-12行的代码与第19-26行的代码相同,后者实际上转换为count++行。finally块被清楚地复制了。
https://stackoverflow.com/questions/29061627
复制相似问题