为什么 Java Compiler copy finally 块? [英] Why does the Java Compiler copy finally blocks?

查看:34
本文介绍了为什么 Java Compiler copy finally 块?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

When compiling the following code with a simple try/finally block, the Java Compiler produces the output below (viewed in the ASM Bytecode Viewer):

代码:

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

When adding a catch block in between, I noticed that the Compiler copied the finally block 3 times (not posting the bytecode again). This seems like a waste of space in the class file. The copying also doesn't seem to be limited to a maximum number of instructions (similar to how inlining works), since it even duplicated the finally block when I added more calls to System.out.println.

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

However, the result of a custom compiler of mine that uses a different approach of compiling the same code works exactly the same when executed, but requires less space by using the GOTO instruction:

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 可以实现吗?这是优化过程的一部分,还是我的编译器做错了?

Why does the Java Compiler (or the Eclipse Compiler) copy the bytecode of the finally block multiple times, even using athrow to rethrow exceptions, when the same semantics can be achieved using goto? Is this part of the optimization process, or is my compiler doing it wrong?

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

(The output in both cases is...)

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

推荐答案

Inlining finally Blocks

您提出的问题已在 http://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on-the-jvm/ (wayback machine web存档链接)

Inlining Finally Blocks

The question your asking has been analyzed in part at http://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on-the-jvm/ (wayback machine web archive link)

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

The post will show an interesting example as well as information such as (quote):

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

finally blocks are implemented by inlining the finally code at all possible exits from the try or associated catch blocks, wrapping the whole thing in essentially a "catch(Throwable)" block that rethrows the exception when it finishes, and then adjusting the exception table such that the catch clauses skip over the inlined finally statements. Huh? (Small caveat: prior to the 1.6 compilers, apparently, finally statements used sub-routines instead of full-on code inlining. But we’re only concerned with 1.6 at this point, so that’s what this applies to).

<小时>

JSR 指令和内联最后

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


The JSR instruction and Inlined Finally

There are differing opinions as to why inlining is used though I have not yet found a definitive one from an official document or source.

有以下3种解释:

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

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

Some believe that finally in-lining is used because JSR/RET did not offer major advantages such as the quote from What Java compilers use the jsr instruction, and what for?

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

The JSR/RET mechanism was originally used to implement finally blocks. However, they decided that the code size savings weren't worth the extra complexity and it got gradually phased out.

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

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

Another possible explanation has been proposed in the comments by @jeffrey-bosboom, who I quote below:

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

javac used to use jsr (jump subroutine) to only write finally code once, but there were some problems related to the new verification using stack map tables. I assume they went back to cloning the code just because it was the easiest thing to do.

必须维护子程序脏位:

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

An interesting exchange in the comments of question What Java compilers use the jsr instruction, and what for? points that JSR and subroutines "added extra complexity from having to maintain a stack of dirty bits for the local variables".

交易所下方:

@paj28:jsr 会不会造成这样的困难,如果它只能调用声明的子程序",每个子程序只能在开始,只能从另一个子程序调用,并且可以仅通过 ret 或突然完成(返回或抛出)退出?复制finally 块中的代码看起来真的很丑,尤其是因为finally 相关的清理可能经常调用嵌套的 try 块.——超级猫 14 年 1 月 28 日 23:18

@paj28: Would the jsr have posed such difficulties if it could only call declared "subroutines", each of which could only be entered at the start, would only be callable from one other subroutine, and could only exit via ret or abrupt completion (return or throw)? Duplicating code in finally blocks seems really ugly, especially since finally-related cleanup may often invoke nested try blocks. – supercat Jan 28 '14 at 23:18

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

@supercat, Most of that is already true. Subroutines can only be entered from the start, can only return from one place, and can only be called from within a single subroutine. The complexity comes from the fact that you have to maintain a stack of dirty bits for the local variables and when returning, you have to do a three-way merge. – Antimony Jan 28 '14 at 23:40

这篇关于为什么 Java Compiler copy finally 块?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆