为什么Java编译器复制最终会阻塞? [英] Why does the Java Compiler copy finally blocks?

查看:110
本文介绍了为什么Java编译器复制最终会阻塞?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当使用简单的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):

代码:

Code:

try
{
    System.out.println("Attempting to divide by zero...");
    System.out.println(1 / 0);
}
finally
{
    System.out.println("Finally...");
}

字节码:

Bytecode:

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块.

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

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

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

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)

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

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种解释:

无报价优势-更多麻烦:

有些人认为最终使用内联是因为JSR/RET没有提供主要优势,例如

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(跳转子例程)仅最终写入一次代码,但是存在一些与使用堆栈映射表进行新验证有关的问题.我认为他们只是因为这是最容易的事情而回到克隆代码.

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.

必须维护子例程脏位:

关于问题> Java编译器使用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仅能做到那么困难吗? 调用已声明的子例程",每个子例程只能在以下位置输入 开始,只能从另一个子例程调用,并且可以 仅通过退出或突然完成(返回或抛出)退出?复制中 最终代码块中的代码看起来确实很难看,尤其是因为 与最终有关的清理可能经常调用嵌套的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编译器复制最终会阻塞?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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