一尘不染

为什么Java编译器复制最终会阻塞?

java

当用一个简单的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
次(不再再次发布字节码)。这似乎在类文件中浪费空间。复制似乎也不限于最大指令数(类似于内联的工作方式),因为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

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


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

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

阅读 205

收藏
2020-12-03

共1个答案

一尘不染

内联最后的块

您提出的问题已在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,因此适用于此)。


JSR指令和内联最后

关于为什么使用内联的观点存在分歧,尽管我尚未从官方文档或来源中找到确定的内联。

有以下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

2020-12-03