为什么 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块时,我注意到编译器复制了 3finally块(不再发布字节码)。这似乎是在浪费类文件中的空间。复制似乎也不限于最大数量的指令(类似于内联的工作方式),因为当我向System.out.println添加更多调用时,它甚至复制了finally块。


但是,我的自定义编译器使用编译相同代码的不同方法的结果在执行时完全相同,但通过使用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...

联最终块

您的问题已在 http://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on-the-jvm/(Wayback Machine Web Archive link)进行了部分分析(Wayback Machine Web Archive link)

这篇文章将展示一个有趣的例子以及诸如(引用)之类的信息:

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


JSR 指令和内联最后

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

有以下3种解释:

没有报价优势 - 更多麻烦:

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

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

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

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

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

必须维护子程序脏位:

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

在交易所下方:

@paj28:如果JSR只能 调用声明为"子例程",每个子例程只能在 开始,只能从另一个子例程调用,并且可以 仅通过 RET 或突然完成(返回或投掷)退出?复制 最终块中的代码看起来真的很丑陋,特别是因为 与最终相关的清理可能经常调用嵌套的 try 块。– 超级猫 1月 28 '14 在 23:18

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

编译

这个:

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行添加一个finally 块,第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开始内联最终阻止,来自Hamilton和Danicic的"当前Java字节码反编译器的评估"[2009]:

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

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

第 5-12 行中的代码与第 19-26 行中的代码相同,实际上转换为 count++ 行。最后一个块显然是复制的。

相关内容

  • 没有找到相关文章

最新更新