首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >为什么Java编译器复制finally块?

为什么Java编译器复制finally块?
EN

Stack Overflow用户
提问于 2015-03-15 22:16:24
回答 2查看 1.9K关注 0票数 22

当使用简单的try/finally块编译以下代码时,Java编译器生成以下输出(在ASM Bytecode Viewer中查看):

代码:

代码语言:javascript
复制
try
{
    System.out.println("Attempting to divide by zero...");
    System.out.println(1 / 0);
}
finally
{
    System.out.println("Finally...");
}

字节码:

代码语言:javascript
复制
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次(不再发布字节码)。这似乎是在浪费类文件中的空间。复制似乎也不限于指令的最大数量(类似于内联的工作原理),因为当我添加更多对System.out.println的调用时,它甚至复制了finally块。

然而,我的一个自定义编译器使用不同的方法编译相同的代码,其结果在执行时完全相同,但通过使用GOTO指令所需的空间更少:

代码语言:javascript
复制
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

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

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

代码语言:javascript
复制
Attempting to divide by zero...
Finally...
EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2015-03-16 00:03:42

内联Finally块

你问的问题已经在http://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on-the-jvm/ (wayback machine web archive链接)上进行了部分分析

这篇文章将展示一个有趣的例子,以及(引用)这样的信息:

Throwable finally块的实现方法是:在try或关联的catch块的所有可能出口内联

代码,将整个代码包装在一个“catch (Throwable)”块中,该块在异常结束时重新抛出异常,然后调整异常表,以便catch子句跳过内联的finally语句。哈?(小提示:在1.6编译器之前,显然,finally语句使用子例程而不是完整的代码内联。但在这一点上,我们只关心1.6,所以这就是适用的)。

JSR指令和Inlined

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

有以下3种解释:

没有提供优势-更多麻烦:

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

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

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

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

javac过去使用jsr (跳转子例程)只写一次最终代码,但是在使用堆栈映射表的新验证中出现了一些问题。我猜想他们又回到了克隆代码,因为这是最容易做的事情。

必须维护子例程脏位的

在问题What Java compilers use the jsr instruction, and what for?的注释中有一个有趣的交换,指出JSR和子例程“由于必须为局部变量维护一堆脏位而增加了额外的复杂性”。

以下是交换:

@paj28:如果jsr只能调用声明的"subroutines",每个子例程只能在开始时进入,只能从另一个子例程调用,并且只能通过ret或突然完成(返回或抛出)退出,那么jsr会带来这样的困难吗?在finally块中复制代码看起来真的很难看,特别是因为与finally相关的清理通常会调用嵌套的try块。- supercat Jan 28 '14在23:18

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

票数 13
EN

Stack Overflow用户

发布于 2015-03-16 00:07:34

编译以下代码:

代码语言:javascript
复制
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的结果,finally块被简单地附加到管理异常的每个部分的末尾(添加-v,在第37行添加了一个finally块,49行的块用于未检查的java.lang.Errors):

代码语言:javascript
复制
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块实现与您提出的类似,但是自从Java1.4.2javac开始内联finally块以来,来自Hamilton &Danicic的"An Evaluation of Current Java Bytecode Decompilers"2009:

许多旧的反编译器都希望使用子例程来执行

-finally代码块,但是javac 1.4.2+生成的是内联代码。

2006年的一篇blog post讨论了这一点:

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

票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/29061627

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档