当前位置: 首页 > 面试题库 >

为什么Java编译器复制最终会阻塞?

濮阳耀
2023-03-14
问题内容

当用一个简单的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在两者之间添加一个块时,我注意到编译器将finally块复制了 3
次(不再再次发布字节码)。这似乎在类文件中浪费空间。复制似乎也不限于最大指令数(类似于内联的工作方式),因为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...

问题答案:

内联最后的块

您提出的问题已在http://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on-
the-
jvm/中

进行了部分分析(回溯机器Web归档链接)

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

通过在try或关联的catch块的所有可能的出口处内联final代码,然后将整个内容包装在本质上是“
catch(Throwable)”块中,该代码在完成时重新抛出异常,然后调整异常表,从而实现了finally块catch子句会跳过内联的finally语句。??(需要注意的是:在1.6编译器之前,显然,finally语句使用子例程而不是完整的代码内联。但是此时我们仅关注1.6,因此适用于此)。

JSR指令和内联最后

关于为什么使用内联的观点存在分歧,尽管我尚未从官方文档或来源中找到确定的内联。

有以下3种解释:

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

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

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

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

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

javac曾经使用jsr(跳转子例程)只编写一次final代码,但是使用堆栈映射表进行新的验证存在一些问题。我认为他们只是因为这是最容易的事情而回到克隆代码。

必须维护子例程脏位:

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

下面交流:

@
paj28:如果jsr只能调用已声明的“子例程”,那么每个jsr都会造成此类困难,每个子例程只能在开始时输入,只能从另一个子例程调用,并且只能通过ret或突然完成退出(返回还是抛出)?在finally块中复制代码确实很丑陋,尤其是由于与final相关的清除操作可能经常调用嵌套的try块。–
2014年1月28日在23:18的超级猫

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



 类似资料:
  • 问题内容: 如果您有这样的程序: 注意两个输出语句之间写的URL 。 为什么程序编译时没有任何错误? 问题答案: 程序编译没有错误的原因是程序将其视为标签,这在Java中是允许的,并且通常与循环一起使用。 第二部分是,是的注释,因此被编译器忽略。 因此,它可以正确编译。

  • 配置选项 要做到最大限度的定制每一个软件包,获取完整的配置选项是必须的。当然,要想更加详细、全面的了解如何自定义安装,还需要查看 README INSTALL FAQ 之类的文档,甚至是软件包的官方手册。需要注意的是,有不少软件包的配置选项分布在多个 configure 脚本中,还有少数并不是通过 configure 脚本进行配置的,查看完整的配置信息就变成一件很吃力的事情了。因此唯一的建议就是:

  • 问题内容: 注意:这不是有关settimeout的复制文章,此处的关键答案是浏览器设计选项。 我开始研究node.js:一个测试异步的简单示例: 一件有趣的事情是,在带有curl的lind命令和浏览器中,它的行为是不同的:在Ubuntu 12.10中,我在两个控制台中使用curl localhost:8080,它们在几乎相同的10个发送中进行响应。 但是,我打开了两个浏览器,几乎同时发出了请求,但

  • 问题内容: 这段代码使我凝视了几分钟: (这里的第137行) 我以前从未见过,而且我也不知道Java有一个“ loop”关键字(NetBeans甚至没有像关键字一样给它上色),并且它在JDK 6中可以很好地编译。 有什么解释? 问题答案: 它不是一个keyword,而是一个label。 用法:

  • 主要内容:什么是编译器,什么是集成开发环境,选择哪种集成开发环境我们平时所说的程序,是指双击后就可以直接运行的程序,这样的程序被称为 可执行程序(Executable Program)。在 Windows 下,可执行程序的后缀有 .exe 和 .com(其中 .exe 比较常见);在类 UNIX 系统(Linux、Mac OS 等)下,可执行程序没有特定的后缀,系统根据文件的头部信息来判断是否是可执行程序。 可执行程序的内部是一系列计算机指令和数据的集合,它们

  • 问题内容: 遇到一个错误地使用 而不是 在其代码中的人,它没有显示为编译错误。 是因为 是相同的 ? 问题答案: 没有编译错误,因为它是有效的(尽管相当无用) 一元运算符 ,其使用方式与以下方式相同: Java语言规范中的相关部分是Unary Plus运算符+(第15.15.3节) 。它指定调用一元运算会导致操作数的一元数值提升(第5.6.1节)。这意味着: * 如果操作数是编译时类型的,,,或,