当前位置: 首页 > 知识库问答 >
问题:

尝试使用资源引入无法访问的字节码

劳法
2023-03-14

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

这是不是意味着这家伙可以永远循环?

共有1个答案

司寇星海
2023-03-14

太长别读:这已经用JDK-11解决了;答案的末尾是JDK-11的javac输出的示例,以供比较。

每个throwable都是java.lang.Throwable的实例,这一事实隐含在Java字节码/JVM的不同位置。即使任何一个的处理程序旨在表示可能在Throwable类型层次结构之外的东西,这个想法也失败了,因为今天的类文件必须有一个StackMapTable用于包含异常处理程序的方法,并且StackMapTable将引用任何throwable作为java.lang.Throwable1的实例。

即使使用旧的类型推断验证器,重新抛出throwable的处理程序也隐式包含这样的断言,即任何throwable都是java的实例。lang.Throwable是唯一允许抛出的对象。

http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.athrow

对象树必须是引用类型,并且必须引用一个对象,该对象是类ThrowableThrowable的子类的实例。

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

现在请阅读说明:

开始很简单,使用了两个局部变量,一个用于保存可自动关闭的(索引0),另一个用于可能的可丢弃的(索引1,用null初始化)<调用code>dummy()和bar(),然后检查AutoCloseable是否为空,以查看是否必须将其关闭。

     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

如果可自动关闭的不是空的,那么我们就到了这里,第一件奇怪的事情发生了,那就是检查可丢弃的是否为空

    13: aload_1
    14: ifnull        35

下面的代码将关闭由上表中的第一个异常处理程序保护的自动关闭,该异常处理程序将调用addSuppressed。因为此时,变量#1为空,这是死代码:

    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,是返回的分支,因此如果上面的代码无论如何都不是死代码,我们可能会开始想,为什么要在一个被忽略的可丢弃的上调用addSuppressed。

现在跟随在变量#1为null(始终读取)时执行的代码。它只是调用关闭并分支到返回指令,不捕获任何异常,因此关闭()引发的异常会传播给调用者:

    35: aload_0
    36: invokeinterface #4,  1   // InterfaceMethod java/lang/AutoCloseable.close:()V
    41: goto          86

现在我们进入第二个异常处理程序,覆盖try语句的主体,声明为catThrowable,读取所有异常。它将Throwable存储到变量#1中,正如预期的那样,但也将其存储到过时的变量#2中。然后它重新抛出Throwable

    44: astore_2
    45: aload_2
    46: astore_1
    47: aload_2
    48: athrow

以下代码是两个异常处理程序的目标。首先,它是多余的任何异常处理程序的目标,该异常处理程序覆盖与可丢弃的处理程序相同的范围,因此,正如您所怀疑的那样,该处理程序什么都不做。此外,它是第四个异常处理程序的目标,捕获任何内容并覆盖上面的异常处理程序,因此我们稍后捕获指令#48右一指令的重新抛出异常。为了让事情变得更有趣,异常处理程序比上面的处理程序涵盖更多内容;以#50结尾(不包括),它甚至涵盖了自身的第一条指令:

    49: astore_3

所以第一件事是引入第三个变量来保存相同的throwable。现在检查AutoCloseable是否null

    50: aload_0
    51: ifnull        84

现在,检查变量#1的抛出是否为null。只有当假设的可丢弃对象不是可丢弃对象时,它才可以为null。但请注意,在这种情况下,验证器将拒绝整个代码,因为StackMapTable声明了所有变量和操作数堆栈项,其中包含与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

最后但并非最不重要的是,我们有一个异常处理程序,当一个挂起的异常存在时,它处理关闭引发的异常,该异常将调用addSubpress并重新抛出主要异常。它引入了另一个局部变量,表明javac确实从不使用交换,即使在适当的情况下。

    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处与常规情况连接。

    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)

顺便说一句,Eclipse生成了更简单的代码,指令序列只需要60个字节,而不是87个字节,只有两个预期的异常处理程序和三个局部变量,而不是五个。在更紧凑的代码中,它处理了可能的情况,即主体引发的异常可能与关闭引发的异常相同,在这种情况下,不能调用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编译版本的冗余更少。它仍然没有检查抛出值是否相同,但我更愿意添加另一个异常处理程序条目,该条目涵盖了addSuppressed调用指令,并将重新抛出的代码定位在40,而不是为这个特殊情况插入预检查。那么,它的代码仍然比其他选择少。

 类似资料:
  • 问题内容: 我一直在看代码,并且看到了尝试资源的机会。我以前使用过标准的try-catch语句,看起来它们在做同样的事情。所以我的问题是“ 尝试使用资源”与“尝试捕获 ”之间的区别是什么,哪个更好。 这是尝试使用资源: 问题答案: 尝试使用资源的重点是确保可靠地关闭资源。 当你不使用try-with-resources时,存在一个潜在的陷阱,称为异常屏蔽。当try块中的代码引发异常,而finall

  • 你好,我正在尝试创建一个类,它使用从学生类到研究生类的继承,但程序说它是不可访问的。 程序应该打印出一个初始化的GraduateStudent类变量,调用printStudent();

  • 我知道,如果资源已实现自动关闭,您通过尝试传递的资源将自动关闭。到现在为止,一直都还不错。但是,当我有几个我想要自动关闭的资源时,我该怎么办呢。带插座的示例; 所以我知道套接字将被正确关闭,因为它在try中作为参数传递,但是输入和输出应该如何正确关闭呢?

  • 我需要打开N个多播套接字(其中N来自参数列表的大小)。然后,我将向循环中的N个套接字中的每个套接字发送相同的数据,最后关闭每个套接字。我的问题是,如何使用try with resources块来实现这一点?以下是我将如何使用单个资源来实现这一点: 我能想到的使用多个端口执行此操作的唯一方法如下: 有没有一种更简洁的方法来实现这一点,或者我提出的解决方案是否尽可能好?

  • 我认为流API在这里是为了使代码更易于阅读。我觉得有点烦。流接口扩展了java。lang.AutoCloseable接口。 因此,如果你想正确地关闭流,你必须使用try-with资源。 清单1.不是很好,流没有关闭。 清单2.使用2嵌套try 清单3。当map返回流时,必须关闭stream()和map()函数。 我举的例子毫无意义。为了示例,我将jpg图像的路径替换为整数。但不要让这些细节分散你的

  • 我得到: 错误:(37,30)Java:无法访问未找到java.util.function.function的java.util.function.function类文件 指向“等待”