为什么Java Compiler副本最终会阻塞?

Cla*_*oft 21 java jvm javac try-catch-finally try-finally

使用简单try/finally块编译以下代码时,Java Compiler将生成以下输出(在ASM Bytecode Viewer中查看):

码:

try
{
    System.out.println("Attempting to divide by zero...");
    System.out.println(1 / 0);
}
finally
{
    System.out.println("Finally...");
}
Run Code Online (Sandbox Code Playgroud)

字节码:

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
Run Code Online (Sandbox Code Playgroud)

catch在中间添加一个块时,我注意到编译器复制了finally3次(不再发布字节码).这似乎浪费了类文件中的空间.复制似乎也不限于最大数量的指令(类似于内联工作方式),因为它甚至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
Run Code Online (Sandbox Code Playgroud)

为什么Java编译器(或Eclipse编译器)finally多次复制块的字节码,甚至athrow用于重新抛出异常,当使用相同的语义时goto?这是优化过程的一部分,还是我的编译器做错了?


(两种情况下的输出都是......)

Attempting to divide by zero...
Finally...
Run Code Online (Sandbox Code Playgroud)

Men*_*los 12

内联最后块

您的询问问题已部分分析在:http://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on-the-jvm/

该帖子将展示一个有趣的例子以及诸如(引用)之类的信息:

finally块是通过在try或关联的catch块的所有可能的出口处内联最终代码来实现的,将整个事物包装在一个"catch(Throwable)"块中,该块在完成时重新抛出异常,然后调整异常表这样catch子句跳过内联的finally语句.咦?(小警告:在1.6编译器之前,显然,最后语句使用子例程而不是全内码代码内联.但是我们此时只关注1.6,所以这适用于此).


JSR指令和Inlined Finally

关于为什么使用内联虽然我还没有从官方文件或来源找到明确的内联,但是有不同的意见.

有以下3种解释:

没有优惠 - 更麻烦:

有些人认为最终使用内衬是因为JSR/RET没有提供主要的优点,例如什么Java编译器使用jsr指令的引用,以及为什么?

JSR/RET机制最初用于实现finally块.然而,他们认为代码大小的节省不值得额外的复杂性,并逐渐被淘汰.

使用堆栈映射表进行验证的问题:

@ jeffrey-bosboom在评论中提出了另一种可能的解释,我在下面引述:

javac曾经使用jsr(跳转子程序)只写一次最终代码,但是使用堆栈映射表的新验证存在一些问题.我假设他们回去克隆代码只是因为这是最简单的事情.

必须维护子程序脏位:

问题评论中有趣的交流什么是Java编译器使用jsr指令,为什么?指出JSR和子程序"必须为局部变量维护一堆脏位"增加了额外的复杂性".

交易所下方:

@ paj28:如果jsr只能调用声明的"子程序",它们只能在开始时输入,只能从另一个子程序中调用,并且只能通过ret或突然完成退出,那么jsr是否会造成这样的困难(回来还是扔?在finally块中复制代码似乎非常难看,特别是因为最终相关的清理可能经常调用嵌套的try块. - 超级猫2014年1月28日23:18

@supercat,其中大部分已经是真的.子程序只能从一开始输入,只能从一个地方返回,并且只能在一个子程序中调用.复杂性来自于您必须为局部变量维护一堆脏位,并且在返回时,您必须进行三向合并. - 锑于2014年1月28日23:40

  • "内联被放弃了".我感到很困惑:是不是正好勾勒出我们在这里得到的东西?我理解这个术语的方式是,当他们不使用`JSR`将finally块"放在行外"时,最后阻塞的内容会多次*内联*. (2认同)