22

使用简单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块时,我注意到编译器复制了该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

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


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

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

2 回答 2

13

内联 finally 块

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

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

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


JSR指令和内联Finally

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

有以下3种解释:

没有优惠的优势 - 更多的麻烦:

一些人认为使用 finally 内联是因为 JSR/RET 没有提供主要优势,例如引用What Java compilers use the jsr instruction, and what for?

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

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

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

javac以前使用jsr(跳转子程序)只写一次finally代码,但是新的使用栈映射表的验证出现了一些问题。我认为他们回到克隆代码只是因为这是最容易做的事情。

必须维护子程序脏位:

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

交易所下方:

@paj28:如果 jsr 只能调用声明的“子例程”,每个子例程只能在开始时输入,只能从另一个子例程调用,并且只能通过 ret 或突然完成退出(返回或抛出)?在 finally 块中复制代码看起来真的很难看,尤其是因为 finally 相关的清理可能经常调用嵌套的 try 块。– 2014 年 1 月 28 日 23:18

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

于 2015-03-15T16:03:42.063 回答
3

编译这个:

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 块只是简单地附加在管理异常的每个部分的末尾(添加 catch,在第 37 行添加了 finally 块,在 49 处的块用于未检查的 java.lang。错误):

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 块实现类似于您所提议的,但自从 Java 1.4.2 javac 开始内联 finally 块,来自Hamilton & Danicic 的“ An Evaluation of Current Java Bytecode Decompilers ”[2009]:

许多旧的反编译器希望将子例程用于 try-finally 块,但 javac 1.4.2+ 会生成内联代码。

2006 年的一篇博客文章讨论了这一点:

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

于 2015-03-15T16:07:33.827 回答