Sun的javac编译器产生的异常表项异常奇怪

56

考虑下面这个程序:

class Test {
    public static void main(String[] args) {
        try {
            throw new NullPointerException();
        } catch (NullPointerException npe) {
            System.out.println("In catch");
        } finally {
            System.out.println("In finally");
        }
    }
}

Sun的javac(版本1.6.0_24)生成以下字节码:

public static void main(java.lang.String[]);

        // Instantiate / throw NPE
   0:   new     #2;         // class NullPointerException
   3:   dup
   4:   invokespecial   #3; // Method NullPointerException."<init>":()V
   7:   athrow

        // Start of catch clause
   8:   astore_1
   9:   getstatic       #4; // Field System.out
   12:  ldc     #5;         // "In catch"
   14:  invokevirtual   #6; // Method PrintStream.println
   17:  getstatic       #4; // Field System.out

        // Inlined finally block
   20:  ldc     #7;         // String In finally
   22:  invokevirtual   #6; // Method PrintStream.println
   25:  goto    39

        // Finally block
        // store "incomming" exception(?)
   28:  astore_2
   29:  getstatic       #4; // Field System.out
   32:  ldc     #7;         // "In finally"
   34:  invokevirtual   #6; // Method PrintStream.println

        // rethrow "incomming" exception
   37:  aload_2
   38:  athrow

   39:  return

以下是异常表:

  Exception table:
   from   to  target type
     0     8     8   Class NullPointerException
     0    17    28   any
    28    29    28   any
我的问题是:这个异常表为什么要包括最后一个条目?! 据我所知,它基本上是在说“如果astore_2抛出异常,则捕获它并重试相同的指令”。
即使是空的try/catch/finally子句,也会生成这样的条目。
try {} catch (NullPointerException npe) {} finally {}

一些观察结果

  • Eclipse编译器不会生成任何这样的异常表项。
  • JVM规范没有记录astore指令的任何运行时异常。
  • 我知道对于任何指令,JVM抛出VirtualMachineError都是合法的。我猜想这个奇怪的条目防止了任何这样的错误从该指令传播出来。

7
由于我还没有理解这个概念,所以我会将这段话发布为评论。关于此主题,有一篇条目在a blog 上对此进行了解释。根据虚拟机规范,编译器编译finally块的行为与Sun/Oracle编译器的实际行为略有不同。最后一个异常表项用于保护“生成的异常处理程序”。我不明白守卫如何运作以及为什么要以这种方式工作。 - Vineet Reynolds
3个回答

8
只有两种可能的解释:编译器存在错误或者它为了某些模糊的原因放置了一种水印。
那个条目肯定是虚假的,因为任何由finally块本身抛出的异常都必须将执行流发送到外部异常处理程序或finally块,但从不会“再次运行”同一个finally块。
此外,证明它是一个bug/水印的有力证据是,Eclipse(和其他Java编译器)没有生成这样的条目,即使是由Eclipse生成的类在Sun的JVM上也能正常工作。
话虽如此,这篇文章很有趣,因为它似乎类文件是有效的并经过验证的。如果我是JVM实现者,我会忽略那个条目并为Sun/Oracle填写一个bug报告!

3
任何在finally块中抛出的异常本身都必须将执行流程发送到外部异常处理程序”--不是这样的,如果你有一个内部catch块的话。此外,您混淆了Java和字节码:字节码程序应该实现要编译的Java程序的语义。它可以通过抛出/捕获各种疯狂的异常来实现。编译程序没有“正确”或“错误”的编译方式。我的怀疑是该条目是编译器中某个更一般构造的副作用。也许这样的条目甚至会在一些罕见的角落案例中从eclipse编译器中发出。 - aioobe
@aioobe 虽然JVM可以成为其他语言的目标,而不仅仅是Java,但这个问题明确展示了一个Java程序被编译成JVM。JVM可能允许异常被抛出的代码处理,但astore_2指令属于没有内部try的Java finally块,所以任何由它引发的异常都必须将执行流发送到外部异常处理程序。“我怀疑这个条目是编译器中更一般构造的副作用。” - 我也是这么认为,但尽管如此,这违反了Java语言的契约。 - fernacolo
这违反了Java语言契约 - 不,它并没有。你可以像这样进行推理:在finally子句中抛出的任何异常都应该传播到该子句之外。因此,如果finally子句中不存在任何语句,则不得从其中传播任何异常。因此,如果编译后的finally块中不包含任何语句,则完全可以在“编译后的finally块”中抑制所有异常,这是完全合法的。 - aioobe
1
@aioobe 是的,但是问题中的finally块确实包含语句!!! 另一种解释为什么它是一个错误:如果某个JVM在astore_2函数上抛出VirtualMachineError(或其他Error异常),则在使用javac编译时,它可能进入消耗100% CPU的无限循环,即使Java代码中没有任何循环。现在,如果使用Eclipse编译相同的类,则VirtualMachineError将正确地将执行流发送到外部。 - fernacolo
@Bringer128,这很有趣。我之前没注意到。 - aioobe
显示剩余2条评论

8
查看OpenJDK 7源代码,我猜测最后一个28 29 28 any异常表条目的原因是处理astore字节码的代码(请参见从第1871行开始的代码)如果从操作数栈中弹出的值不是returnAddressreference类型,则可以抛出java.lang.LinkageError异常(请参见Java虚拟机规范中有关astore的说明),他们希望这种错误条件在堆栈跟踪中显示。
如果操作数栈上有错误的操作数类型,则JVM将清除操作数栈(清除该错误操作数),将LinkageError放入操作数栈,并再次执行astore字节码,这次使用JVM提供的LinkageError对象引用成功地执行astore字节码。有关更多信息,请参见athrow文档。
我非常怀疑在astore处理期间抛出LinkageError的根本原因是由于JSR/RET子例程复杂性引入了字节码验证(OpenJDK更改687871369324967020373是证明了JSR的持续复杂性; 我相信Sun / Oracle拥有其他我们没有在OpenJDK中看到的闭源测试)。 OpenJDK 7020373更改使用LinkageError来验证/无效化测试结果。

嗯...这种类型错误不是在字节码验证阶段就会被捕获吗?(在执行代码之前。) - aioobe
我的答案已更新,包括关于JSR/RET子例程复杂性的信息,这使得字节码验证变得痛苦。 - Go Dan

1

我的理解是第二个异常表条目是编译器添加的隐式捕获所有在主体或catch处理程序中抛出的任何异常/错误的条款,而第三个条目是对该隐式catch的保护,以强制执行finally语句块。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接