当用一个简单的try/finally块编译以下代码时,Java编译器将产生以下输出(在ASM字节码查看器中查看):
try/finally
码:
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 次(不再再次发布字节码)。这似乎在类文件中浪费空间。复制似乎也不限于最大指令数(类似于内联的工作方式),因为finally当我向添加更多调用时,它甚至复制了该块System.out.println。
catch
finally
System.out.println
但是,我的自定义编译器使用不同的方法编译相同代码的结果在执行时完全相同,但是使用以下GOTO指令所需的空间更少:
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多次复制块的字节码,甚至athrow用于重新抛出异常goto?这是优化过程的一部分,还是我的编译器做错了?
athrow
goto
(两种情况下的输出都是…)
Attempting to divide by zero... Finally...
您提出的问题已在http://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on- the- jvm/中进行了部分分析(回溯机器Web归档链接)
该帖子将显示一个有趣的示例以及诸如(quote)之类的信息:
通过在try或关联的catch块的所有可能的出口处内联final代码,然后将整个内容包装在本质上是“ catch(Throwable)”块中,该代码在完成时重新抛出异常,然后调整异常表,从而实现了finally块catch子句会跳过内联的finally语句。??(需要注意的是:在1.6编译器之前,显然,finally语句使用子例程而不是完整的代码内联。但是此时我们仅关注1.6,因此适用于此)。
关于为什么使用内联的观点存在分歧,尽管我尚未从官方文档或来源中找到确定的内联。
有以下3种解释:
没有报价优势-麻烦更多:
有些人认为最终使用内联是因为JSR /RET并没有提供主要优势,例如引述了哪些Java编译器使用jsr指令,以及为什么?
JSR / RET机制最初用于实现finally块。但是,他们认为节省代码大小并不值得额外的复杂性,因此逐渐被淘汰了。
使用堆栈映射表进行验证的问题:
@ jeffrey-bosboom在评论中提出了另一种可能的解释,我在下面引用:
javac曾经使用jsr(跳转子例程)只编写一次final代码,但是使用堆栈映射表进行新的验证存在一些问题。我认为他们只是因为这是最容易的事情而回到克隆代码。
必须维护子例程脏位:
在问题注释中进行了有趣的交流,哪些Java编译器使用jsr指令,以及用于什么目的?指出,JSR和子例程“由于必须为局部变量维护一堆脏位而增加了额外的复杂性”。
下面交流:
@ paj28:如果jsr只能调用已声明的“子例程”,那么每个jsr都会造成此类困难,每个子例程只能在开始时输入,只能从另一个子例程调用,并且只能通过ret或突然完成退出(返回还是抛出)?在finally块中复制代码确实很丑陋,尤其是由于与final相关的清除操作可能经常调用嵌套的try块。– 2014年1月28日在23:18的超级猫 @supercat,大多数已经是真的。子例程只能从头开始输入,只能从一个位置返回,并且只能在单个子例程中调用。 复杂性来自以下事实:您必须维护一堆用于局部变量的脏位,并且在返回时必须进行三向合并。 –锑2014年1月28日23:40
@ paj28:如果jsr只能调用已声明的“子例程”,那么每个jsr都会造成此类困难,每个子例程只能在开始时输入,只能从另一个子例程调用,并且只能通过ret或突然完成退出(返回还是抛出)?在finally块中复制代码确实很丑陋,尤其是由于与final相关的清除操作可能经常调用嵌套的try块。– 2014年1月28日在23:18的超级猫
@supercat,大多数已经是真的。子例程只能从头开始输入,只能从一个位置返回,并且只能在单个子例程中调用。 复杂性来自以下事实:您必须维护一堆用于局部变量的脏位,并且在返回时必须进行三向合并。 –锑2014年1月28日23:40