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

当使用一个简单的块编译以下代码时,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

在两者之间添加一个块时,我注意到编译器复制了该块3次(不再发布字节码)。这似乎是在类文件中浪费空间。复制似乎也不限于最大数量的指令(类似于内联的工作方式),因为当我添加更多调用时,它甚至复制了块。catchfinallyfinallySystem.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编译器)多次复制块的字节码,甚至用于重新抛出异常,而使用相同的语义可以使用?这是优化过程的一部分,还是我的编译器做错了?finallyathrowgoto


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

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

答案 1

内联最终块

您提出的问题已在 http://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on-the-jvm/(时光机Web存档链接)中进行了部分分析

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

最后,块是通过在 try 或关联的 catch 块的所有可能退出处内联 final 代码来实现的,将整个内容包装在本质上是一个“catch(Throwable)”块中,该块在异常完成时重新抛出该块,然后调整异常表,以便 catch 子句跳过内联的 final 语句。哼?(小警告:在1.6编译器之前,显然,语句最终使用子例程而不是完整的代码内联。但是我们目前只关心1.6,所以这就是适用的)。


JSR 指令和内联最终

对于为什么使用内联有不同的意见,尽管我还没有从官方文件或来源找到明确的内联。

有以下3种解释:

无报价优势 - 更多麻烦:

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

JSR/RET 机制最初用于实现最终的块。但是,他们认为节省代码大小不值得额外的复杂性,因此逐渐被淘汰。

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

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

javac过去使用jsr(跳转子例程)只编写一次最终代码,但是存在一些与使用堆栈映射表的新验证相关的问题。我认为他们回到克隆代码只是因为这是最简单的事情。

必须维护子例程脏位:

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

在交易所下面:

@paj28:如果 jsr 只能调用声明的“子例程”,每个子例程只能在开始时输入,只能从另一个子例程调用,并且只能通过 ret 或突然完成(返回或抛出)退出,那么它是否会造成这样的困难?在 finally 块中复制代码似乎非常丑陋,特别是因为与 final 相关的清理可能会经常调用嵌套的 try 块。– 超级猫 Jan 28 '14 在 23:18

@supercat,其中大部分已经是正确的。子例程只能从头开始输入,只能从一个位置返回,并且只能从单个子例程中调用。复杂性来自这样一个事实,即您必须为局部变量维护一堆脏位,并且在返回时,您必须进行三向合并。– 锑 Jan 28 '14 在 23:40


答案 2

编译这个:

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块只是简单地附加到管理异常的每个部分的末尾(添加捕获,在第37行添加一个最终块,在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

看起来最初的最终块实现类似于你所提议的,但是自从Java 1.4.2 javac开始内联最终块以来,来自Hamilton & Danicic的“当前Java字节码反编译器的评估”[2009]:

许多旧的反编译器期望使用子例程来尝试最终块,但javac 1.4.2 +会生成内联代码。

2006年的一篇博客文章讨论了这个问题:

第 5-12 行中的代码与第 19-26 行中的代码相同,后者实际上转换为 count++ 行。最后的块被清楚地复制了。


推荐