尝试使用资源引入无法访问的字节码 [英] try with resources introduce unreachable bytecode

查看:25
本文介绍了尝试使用资源引入无法访问的字节码的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

即使使用旧的类型推断验证器,重新抛出 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 class Throwable or of a subclass of Throwable.

简短回答:不,不可能出现除了 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 throwablejava.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屋!

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