try with resources introduce unreachable bytecode

后端 未结 1 1536
清歌不尽
清歌不尽 2020-11-27 21:24

Is it possible that javac generates unreachable bytecode for the following procedure?

public void ex06(String name) throws Exception {
    File config = new          


        
相关标签:
1条回答
  • 2020-11-27 22:20

    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.

    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.

    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

    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.

    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.

    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.

    The example looks like this:

    public static void tryWithAuto() throws Exception {
        try (AutoCloseable c=dummy()) {
            bar();
        }
    }
    private static AutoCloseable dummy() {
        return null;
    }
    private static void bar() {
    }
    

    (I compiled with 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
    

    Now to the instructions:

    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
    

    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
    

    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
    

    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.

    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
    

    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
    

    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
    

    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
    

    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
    

    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
    

    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
    

    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.


    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
    

    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
    

    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.

    0 讨论(0)
提交回复
热议问题