javac是否有可能为以下过程生成无法访问的字节码?
public void ex06(String name) throws Exception {
File config = new File(name);
try (FileOutputStream fos = new FileOutputStream(config);
PrintWriter writer = new PrintWriter(new OutputStreamWriter(
fos , "rw"))) {
bar();
}
}
当我查看字节码的异常表(javap -v)时,有以下条目看起来很奇怪:
43 48 86 Class java/lang/Throwable
43 48 95 any
和
21 135 170 Class java/lang/Throwable
21 135 179 any
现在的问题是,如果捕获了类型为“any”而不是Throwable的异常,则只能访问某些代码。有没有可能发生这种情况的情况?
======编辑======感谢目前为止的答案。让我给出另一个证据来表明我真的不理解异常处理:考虑以下过程
Object constraintsLock;
private String[] constraints;
private String constraint;
public void fp01() {
// Add this constraint to the set for our web application
synchronized (constraintsLock) {
String results[] =
new String[constraints.length + 1];
for (int i = 0; i < constraints.length; i++)
results[i] = constraints[i];
results[constraints.length] = constraint;
constraints = results;
}
}
如果你查看字节码:
65: astore 4
67: aload_1
68: monitorexit
69: aload 4
和异常表
Exception table:
from to target type
7 62 65 any
65 69 65 any
这是否意味着这个人可以永远循环?
TL; DR:用JDK-11解决了这个问题;在答案的最后是JDK-11的javac
输出的一个例子用于比较。
每个throwable都是java.lang.Throwable
实例的事实隐含在Java字节代码/ JVM的不同位置。即使任何处理程序用于表示可能在Throwable
类型层次结构之外的东西,该想法也会失败,因为今天的类文件必须具有包含异常处理程序的方法的StackMapTable
,并且StackMapTable
将引用任何throwable作为java.lang.Throwable
1的实例。
即使使用旧类型推断验证程序,重新抛出throwable的处理程序也隐含地包含断言,即任何throwable都是java.lang.Throwable
的实例,因为这是唯一允许athrow
抛出的对象。
http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.athrow
objectref必须是
reference
类型,并且必须引用一个对象,该对象是类Throwable
或Throwable
的子类的实例。
简短的回答:不,不可能有一种情况,除了java.lang.Throwable
(或子类)的实例之外的其他东西可以被抛出或捕获。
我尝试创建一个try-with-resource语句的最小示例来分析javac
的输出。结果清楚地表明,该结构是javac
如何在内部工作但不能是故意的人工制品。
示例如下所示:
public static void tryWithAuto() throws Exception {
try (AutoCloseable c=dummy()) {
bar();
}
}
private static AutoCloseable dummy() {
return null;
}
private static void bar() {
}
(我用jdk1.8.0_20
编译)
我将异常处理程序表放在结果字节代码的开头,这样在查看指令序列时更容易引用该位置:
Exception table:
from to target type
17 23 26 Class java/lang/Throwable
6 9 44 Class java/lang/Throwable
6 9 49 any
58 64 67 Class java/lang/Throwable
44 50 49 any
现在来说明:
开头是直截了当的,使用了两个局部变量,一个用于保持AutoCloseable
(索引0),另一个用于可能的throwable(索引1,用null
初始化)。调用dummy()
和bar()
,然后检查AutoCloseable
是否必须关闭null
。
0: invokestatic #2 // Method dummy:()Ljava/lang/AutoCloseable;
3: astore_0
4: aconst_null
5: astore_1
6: invokestatic #3 // Method bar:()V
9: aload_0
10: ifnull 86
我们到达这里如果AutoCloseable
不是null
并且第一个奇怪的事情发生,绝对null
的throwable被检查为null
13: aload_1
14: ifnull 35
下面的代码将关闭AutoCloseable
,由上表中的第一个异常处理程序保护,它将调用addSuppressed
。从那时起,变量#1是null
这是死代码:
17: aload_0
18: invokeinterface #4, 1 // InterfaceMethod java/lang/AutoCloseable.close:()V
23: goto 86
26: astore_2
27: aload_1
28: aload_2
29: invokevirtual #6 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
32: goto 86
请注意,死代码的最后一条指令是goto 86
,它是return
的一个分支,所以如果上面的代码不是死代码,我们可以开始想知道为什么在addSuppressed
上调用Throwable
后会被忽略。
如果变量#1是null
(read,always),则遵循执行的代码。它只是调用close
并分支到return
指令,而不是捕获任何异常,因此close()
抛出的异常传播给调用者:
35: aload_0
36: invokeinterface #4, 1 // InterfaceMethod java/lang/AutoCloseable.close:()V
41: goto 86
现在我们进入第二个异常处理程序,覆盖try
语句的主体,声明捕获Throwable
,读取所有异常。它将Throwable
存储到变量#1中,如预期的那样,但也将它存储到过时的变量#2中。然后它重新抛出Throwable
。
44: astore_2
45: aload_2
46: astore_1
47: aload_2
48: athrow
以下代码是两个异常处理程序的目标。首先,它是多余的任何异常处理程序的目标,它覆盖与Throwable
处理程序相同的范围,因此,正如您所怀疑的,此处理程序不执行任何操作。此外,它是第四个异常处理程序的目标,捕获任何内容并覆盖上面的异常处理程序,因此我们稍后在一条指令后捕获指令#48的重新抛出的异常。为了使事情更有趣,异常处理程序涵盖的不仅仅是上面的处理程序;结束于#50,独家,它甚至涵盖了自己的第一条指令:
49: astore_3
所以第一件事是引入第三个变量来保持相同的throwable。现在AutoCloseable
检查null
。
50: aload_0
51: ifnull 84
现在检查变量#1的throwable for null
。只有当假想的扔掉不是null
时,它才能成为Throwable
。但请注意,在这种情况下,验证程序将拒绝整个代码,因为StackMapTable
声明所有变量和操作数堆栈条目保持任何throwable与java.lang.Throwable
兼容
54: aload_1
55: ifnull 78
58: aload_0
59: invokeinterface #4, 1 // InterfaceMethod java/lang/AutoCloseable.close:()V
64: goto 84
最后但并非最不重要的是,我们有一个异常处理程序,它处理一个挂起异常时抛出异常抛出的异常,它将调用addSuppressed
并重新抛出主异常。它引入了另一个局部变量,即使在适当的情况下也表明javac
indeed never uses swap
。
67: astore 4
69: aload_1
70: aload 4
72: invokevirtual #6 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
75: goto 84
因此,只有在捕获任何可能暗示除了java.lang.Throwable
之外的其他内容时,才会调用以下两条指令。代码路径在#84处与常规案例连接。
78: aload_0
79: invokeinterface #4, 1 // InterfaceMethod java/lang/AutoCloseable.close:()V
84: aload_3
85: athrow
86: return
所以底线是任何一个额外的异常处理程序只负责四个指令的死代码,#54,#55,#78和#79,而其他原因甚至有更多的死代码(#17 - #32) ,加上一个奇怪的“抛出 - 捕获”(#44 - #48)代码,这可能也是想法处理任何不同于Throwable
的神器。此外,一个异常处理程序具有覆盖自身的错误范围,其可能与Strange exception table entry produced by Sun's javac的“suggested in the comments”相关。
作为旁注,Eclipse生成更简单的代码,仅占用60个字节而不是87个指令序列,只有两个预期的异常处理程序和三个局部变量而不是五个。在更紧凑的代码中,它处理可能的情况,即正文抛出的异常可能与close
抛出的异常相同,在这种情况下不得调用addSuppressed
。 javac
生成的代码并不关心这一点。
0: aconst_null
1: astore_0
2: aconst_null
3: astore_1
4: invokestatic #18 // Method dummy:()Ljava/lang/AutoCloseable;
7: astore_2
8: invokestatic #22 // Method bar:()V
11: aload_2
12: ifnull 59
15: aload_2
16: invokeinterface #25, 1 // InterfaceMethod java/lang/AutoCloseable.close:()V
21: goto 59
24: astore_0
25: aload_2
26: ifnull 35
29: aload_2
30: invokeinterface #25, 1 // InterfaceMethod java/lang/AutoCloseable.close:()V
35: aload_0
36: athrow
37: astore_1
38: aload_0
39: ifnonnull 47
42: aload_1
43: astore_0
44: goto 57
47: aload_0
48: aload_1
49: if_acmpeq 57
52: aload_0
53: aload_1
54: invokevirtual #30 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
57: aload_0
58: athrow
59: return
Exception table:
from to target type
8 11 24 any
4 37 37 any
从JDK-11开始,javac
将示例编译为
Code:
0: invokestatic #2 // Method dummy:()Ljava/lang/AutoCloseable;
3: astore_0
4: invokestatic #3 // Method bar:()V
7: aload_0
8: ifnull 42
11: aload_0
12: invokeinterface #4, 1 // InterfaceMethod java/lang/AutoCloseable.close:()V
17: goto 42
20: astore_1
21: aload_0
22: ifnull 40
25: aload_0
26: invokeinterface #4, 1 // InterfaceMethod java/lang/AutoCloseable.close:()V
31: goto 40
34: astore_2
35: aload_1
36: aload_2
37: invokevirtual #6 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
40: aload_1
41: athrow
42: return
Exception table:
from to target type
4 7 20 Class java/lang/Throwable
25 31 34 Class java/lang/Throwable
它现在具有比ECJ编译版本更少的冗余。它仍然没有检查throwable是否相同,但我宁愿添加另一个异常处理程序条目,覆盖addSuppressed
调用指令并以40
为目标重新投掷代码,而不是插入此角点案例的预检查。然后,它仍然会比替代品更少的代码。