尝试使用资源引入无法访问的字节码 [英] try with resources introduce unreachable bytecode
问题描述
javac 是否有可能为以下过程生成无法访问的字节码?
Is it possible that javac generates unreachable bytecode for the following procedure?
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) 时,有以下条目看起来很奇怪:
When I look into the exception table of the bytecode (javap -v) there are the following entries that look strange:
43 48 86 Class java/lang/Throwable
43 48 95 any
和
21 135 170 Class java/lang/Throwable
21 135 179 any
现在的问题是,某些代码只有在捕获any"而不是 Throwable 类型的异常时才能访问.是否有任何情况可能会发生这种情况?
Now the problem is that some code is only reachable if the exceptions of type "any" rather than Throwable are caught. Is there any situation where this can actually happen?
====== 编辑 ======感谢您到目前为止的答案.让我再举一个证据来表明我真的不了解异常处理:考虑以下程序
====== EDIT ====== Thanks for the answers so far. Let me give another piece of evidence to show that I really don't understand exception handling: Consider the following procedure
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
这是否意味着这家伙可以永远循环下去?
Does that mean that this guy can loop forever?
推荐答案
TL;DR:JDK-11 已经解决了这个问题;答案的最后是 JDK-11 的 javac
输出示例以供比较.
TL;DR: this has been addressed with JDK-11; at the end of the answer is an example of JDK-11’s javac
output for comparison.
事实上,每个 throwable 都是 java.lang.Throwable
的一个实例,这在 Java 字节码/JVM 的各个地方都隐含着.即使 any 的处理程序旨在表示可能在 Throwable
类型层次结构之外的东西,这个想法也失败了,因为今天的类文件必须有一个 StackMapTable
用于包含异常处理程序的方法,并且 StackMapTable
将引用 any throwable 作为 java.lang.Throwable
1.
The fact that every throwable is an instance of java.lang.Throwable
is implied at various places of the Java byte code/ JVM. Even if handlers for any were meant to represent something possibly outside the Throwable
type hierarchy, that idea fails as today’s class files must have a StackMapTable
for methods containing exception handlers and that StackMapTable
will refer to the any throwable as an instance of java.lang.Throwable
1.
即使使用旧的类型推断验证器,重新抛出 throwable 的处理程序隐式包含断言 any throwable 是 java.lang.Throwable
的实例这是唯一允许 athrow
抛出的对象.
Even with the old type inferring verifier, a handler which re-throws a throwable implicitly contains the assertion that any throwable is an instance of java.lang.Throwable
as that’s the only object athrow
is allowed to throw.
http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.athrow
objectref 必须属于 reference
类型,并且必须引用一个对象,该对象是 Throwable
类或 Throwable
子类的一个实例code>Throwable.
The objectref must be of type
reference
and must refer to an object that is an instance of classThrowable
or of a subclass ofThrowable
.
简短回答:不,不可能出现除了 java.lang.Throwable
(或子类)的实例之外的其他东西可以被抛出或捕获的情况.
Short answer: no, it is impossible to have a situation where something other than an instance of java.lang.Throwable
(or a subclass) can be thrown or caught.
我尝试创建一个 try-with-resource 语句的最小示例来分析 javac
的输出.结果清楚地表明,该结构是 javac
内部如何工作的产物,但不能是故意的.
I tried to create a minimal example of a try-with-resource statement to analyse the output of javac
. The result clearly shows that the structure is an artifact of how javac
works internally but can’t be intentional.
示例如下所示:
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
编译)
我将异常处理程序表放在结果字节码的开头,以便在查看指令序列时更容易引用该位置:
I put the exception handler table at the beginning of the resulting byte code so it’s easier to refer to the location while looking at the instruction sequence:
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
是否必须关闭.
The beginning is straightforward, two local variables are used, one to hold the AutoCloseable
(index 0), the other for the possible throwable (index 1, initialized with null
). dummy()
and bar()
are invoked, then the AutoCloseable
is checked for null
to see whether it must be closed.
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 是否<代码>空代码>
We get here if the AutoCloseable
is not null
and the first weird thing happens, the throwable which is definitely null
is checked for null
13: aload_1
14: ifnull 35
以下代码将关闭 AutoCloseable
,由上表中的第一个异常处理程序保护,该异常处理程序将调用 addSuppressed
.由于此时变量 #1 是 null
这是死代码:
The following code will close the AutoCloseable
, guarded by the first exception handler from the table above which will invoke addSuppressed
. Since at this point, variable #1 is null
this is dead-code:
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
的分支,所以如果上面的代码无论如何都不是死代码,我们可能会开始怀疑为什么要在之后被忽略的 Throwable
上调用 addSuppressed
.
Note that the last instruction of the dead code is goto 86
, a branch to a return
so if the code above was not dead code anyway, we could start wondering why bother invoking addSuppressed
on a Throwable
that is ignored right afterwards.
现在遵循如果变量 #1 为 null
(读取,始终)时执行的代码.它只是调用 close
并跳转到 return
指令,不捕获任何异常,因此 close()
抛出的异常会传播给调用者:
Now follows the code that is executed if variable #1 is null
(read, always). It simply invokes close
and branches to the return
instruction, not catching any exception, so an exception thrown by close()
propagates to the caller:
35: aload_0
36: invokeinterface #4, 1 // InterfaceMethod java/lang/AutoCloseable.close:()V
41: goto 86
现在我们进入第二个异常处理程序,覆盖了try
语句的主体,声明捕获Throwable
,读取所有异常.正如预期的那样,它将 Throwable
存储到变量 #1 中,但也将其存储到过时的变量 #2 中.然后它重新抛出Throwable
.
Now we enter the second exception handler, covering the body of the try
statement, declared to catch Throwable
, read all exceptions. It stores the Throwable
into the variable #1, as expected, but also stores it into the obsolete variable #2. Then it re-throws the Throwable
.
44: astore_2
45: aload_2
46: astore_1
47: aload_2
48: athrow
以下代码是两个异常处理程序的目标.首先,它是覆盖与 Throwable
处理程序相同范围的多余 any 异常处理程序的目标,因此,正如您所怀疑的,这个处理程序不做任何事情.此外,它是第四个异常处理程序的目标,捕获任何内容并覆盖上面的异常处理程序,因此我们在下一指令后捕获指令#48 重新抛出的异常.更有趣的是,异常处理程序涵盖的内容比上面的处理程序更多;以#50 结尾,独占,甚至覆盖了自身的第一条指令:
The following code is the target of two exception handlers. First, its the target of the superfluous any exception handler that covers the same range as the Throwable
handler, hence, as you suspected, this handler doesn’t do anything. Further, it is the target of the fourth exception handler, catching anything and covering the exception handler above so we catch the re-thrown exception of instruction #48 right one instruction later. To make things even more funny, the exception handler covers more than the handler above; ending at #50, exclusive, it even covers the first instruction of itself:
49: astore_3
所以首先要引入第三个变量来保存相同的 throwable.现在 AutoCloseable
检查 null
.
So the first thing is to introduce a third variable to hold the same throwable. Now the the AutoCloseable
is checked for null
.
50: aload_0
51: ifnull 84
现在检查变量 #1 的 throwable 是否有 null
.仅当假设的 throwable 不是 Throwable
存在时,它才可以是 null
.但请注意,在这种情况下,整个代码将被验证器拒绝,因为 StackMapTable
声明所有变量和操作数堆栈条目包含 any throwable 与 java.lang.Throwable
Now the throwable of variable #1 is checked for null
. It can be null
only if the hypothetical throwable not being a Throwable
exists. But note that the entire code would be rejected by the verifier in that case as the StackMapTable
declares all variables and operand stack entries holding the any throwable to be assignment compatible to 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
最后但并非最不重要的是,我们有异常处理程序,当存在挂起的异常时,它会处理 close 抛出的异常,该异常将调用 addSuppressed
并重新抛出主要异常.它引入了另一个局部变量,表明 javac
确实从不使用 swap
即使在适当的地方.
Last but not least we have the exception handler which handles exception thrown by close when a pending exception exists which will invoke addSuppressed
and re-throws the primary exception. It introduces another local variables which indicates that javac
indeed never uses swap
even where appropriate.
67: astore 4
69: aload_1
70: aload 4
72: invokevirtual #6 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
75: goto 84
因此,仅当 catch any 可能暗示除 java.lang.Throwable
之外的其他内容时才会调用以下两条指令,但事实并非如此.代码路径在#84 处与常规情况连接.
So the following two instructions are only invoked if catch any could imply something other than java.lang.Throwable
which is not the case. The code path joins at #84 with the regular case.
78: aload_0
79: invokeinterface #4, 1 // InterfaceMethod java/lang/AutoCloseable.close:()V
84: aload_3
85: athrow
86: return
所以底线是any的额外异常处理程序只负责四条指令的死代码,#54、#55、#78和#79,而还有更多的死代码出于其他原因(#17 - #32),加上一个奇怪的投掷和接住"(#44 - #48)代码,这也可能是处理任何的想法的产物,而不是可扔
.此外,一个异常处理程序有一个错误的覆盖范围,这可能与Sun 的 javac 生成的奇怪的异常表条目"作为建议在评论.
So the bottom line is that the additional exception handler for any is responsible for dead code of four instructions only, #54, #55, #78 and #79 while there is even more dead code for other reasons (#17 - #32), plus a strange "throw-and-catch" (#44 - #48) code which might also be an artifact of the idea to handle any differently than Throwable
. Further, one exception handler has a wrong range covering itself which might be related to "Strange exception table entry produced by Sun's javac" as suggested in the comments.
作为旁注,Eclipse 生成了更直接的代码,指令序列只占用 60 个字节而不是 87 个字节,只有两个预期的异常处理程序和三个局部变量而不是五个.在更紧凑的代码中,它处理可能的情况,即主体抛出的异常可能与 close
抛出的异常相同,在这种情况下,不得调用 addSuppressed
.javac
生成的代码不关心这个.
As a side-note, Eclipse produces more straightforward code taking only 60 bytes rather than 87 for the instruction sequence, having the two expected exception handlers only and three local variables instead of five. And within that more compact code it handles the possible case that the exception thrown by the body might be the same as the one throw by close
in which case addSuppressed
must not be called. The javac
generated code does not care for this.
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
将示例编译为
Starting with JDK-11, javac
compiles the example to
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
,而不是为这种极端情况插入预检查.那么,它仍然会比替代方案更少的代码.
It now has even less redundancy than the ECJ compiled version. It still doesn’t check whether the throwables are the same, but I’d rather add another exception handler entry covering the addSuppressed
invocation instruction and targeting the re-throwing code at 40
, rather than inserting a pre-check for this corner case. Then, it would still be less code than the alternatives.
这篇关于尝试使用资源引入无法访问的字节码的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!