美文网首页
Java虚拟机-异常的处理

Java虚拟机-异常的处理

作者: 贪睡的企鹅 | 来源:发表于2019-08-03 23:46 被阅读0次

    1 异常处理

    异常处理的两大组成要素是抛出异常捕获异常。这两大要素共同实现程序控制流的非正常转移。

    2 抛出异常

    抛出异常可分为显式隐式两种。

    • 显式抛异常的主体是应用程序,它指的是在程序中使用“throw”关键字,手动将异常实例抛出。

    • 隐式抛异常的主体则是 Java 虚拟机,它指的是 Java 虚拟机在执行过程中,碰到无法继续执行的异常状态,自动抛出异常。举例来说,Java 虚拟机在执行读取数组操作时,发现输入的索引值是负数,故而抛出数组索引越界异常(ArrayIndexOutOfBoundsException)。

    3 捕获异常

    捕获异常则涉及了如下三种代码块。

    • try 代码块:用来标记需要进行异常监控的代码。
    • catch 代码块:跟在 try 代码块之后,用来捕获在 try 代码块中触发的某种指定类型的异常。除了声明所捕获异常的类型之外,catch 代码块还定义了针对该异常类型的异常处理器。在 Java 中,try 代码块后面可以跟着多个 catch 代码块,来捕获不同类型的异常。Java 虚拟机会从上至下匹配异常处理器。因此,前面的 catch 代码块所捕获的异常类型不能覆盖后边的,否则编译器会报错。
    • finally 代码块:跟在 try 代码块和 catch 代码块之后,用来声明一段必定运行的代码。它的设计初衷是为了避免跳过某些关键的清理代码,例如关闭已打开的系统资源。
    3.1 案例
    public class TryCatchFinally {
        private String tryBlock = "tryBlock";
        private String catchBlock = "catchBlock";
        private String finallyBlock = "finallyBlock";
    
        public String test() {
            try {
                System.out.println(tryBlock);
                //int a = 5/0;  //异常抛出点
                return tryBlock;
            } catch (Exception e) {
                //int a = 5/0;  //异常抛出点
                System.out.println(catchBlock);
                return catchBlock;
            } finally {
                //int a = 5/0;  //异常抛出点
                System.out.println(finallyBlock);
                //return finallyBlock;
            }
        }
    
        public static void main(String[] args) {
            System.out.println(new TryCatchFinally().test());
        }
    }
    
    3.1 程序正常运行
    tryBlock
    finallyBlock
    tryBlock    
    
    3.2 程序异常运行
    打开try中注释//int a = 5/0;
    
    tryBlock
    catchBlock
    finallyBlock
    catchBlock    
    
    3.2 finally中返回

    无论正常运行还是异常运行都返回"finallyBlock"

    打开注释 //return finallyBlock;
    
    //正常运行
    tryBlock
    finallyBlock
    finallyBlock
    
    //异常运行
    tryBlock
    catchBlock
    finallyBlock
    finallyBlock 
    
    3.2 finally中抛出异常

    中断finally中语句,对外抛出异常

    打开finally中注释//int a = 5/0;
    
    //正常运行
    tryBlock
    
    //异常运行
    tryBlock
    catchBlock
    
    3.2 catch中抛出异常

    中止发生异常后中语句,执行finally块中语句,并对外抛出异常

    打开catch中注释//int a = 5/0;
    打开try中注释//int a = 5/0;
    
    tryBlock
    finallyBlock
    

    4 throws声明异常

    使用throws声明在方法上声明可能出现的异常。throws是另一种处理异常的方式,它不同于try...catch...finally,throws仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。

    throws声明:如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异常,否则编译不通过。

    public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN
    { 
         //foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。
    }
    

    4 异常类型

    在 Java 语言规范中,所有异常都是 Throwable 类或者其子类的实例。Throwable 有两大直接子类。第一个是 Error,涵盖程序不应捕获的异常。第二子类则是 Exception,涵盖程序可能需要捕获并且处理的异常。

    image

    Exception 有一个特殊的子类 RuntimeException,用来表示“程序虽然无法继续执行,但是还能抢救一下”的情况。前边提到的数组索引越界便是其中的一种。

    RuntimeException 和 Error 属于 Java 里的非检查异常(unchecked exception)。其他异常则属于检查异常(checked exception)。在 Java 语法中,所有的检查异常都需要程序显式地捕获,或者在方法声明中用 throws 关键字标注。通常情况下,程序中自定义的异常应为检查异常,以便最大化利用 Java 编译器的编译时检查。

    5 异常的链化

    查看Throwable类源码,可以发现里面有一个Throwable字段cause,就是以一个异常对象为参数构造新的异常对象。

    public class Throwable implements Serializable {
        private Throwable cause = this;
       
        public Throwable(String message, Throwable cause) {
            fillInStackTrace();
            detailMessage = message;
            this.cause = cause;
        }
         public Throwable(Throwable cause) {
            fillInStackTrace();
            detailMessage = (cause==null ? null : cause.toString());
            this.cause = cause;
        }
        
        //........
    }
    

    案例

    try {
    
        }catch(InputMismatchException immExp){
            throw new Exception("计算失败",immExp);  /////////////////////////////链化:以一个异常对象为参数构造新的异常对象。
        }
    
    java.lang.Exception: 计算失败
        at practise.ExceptionTest.add(ExceptionTest.java:53)
        at practise.ExceptionTest.main(ExceptionTest.java:18)
    Caused by: java.util.InputMismatchException
        at java.util.Scanner.throwFor(Scanner.java:864)
        at java.util.Scanner.next(Scanner.java:1485)
        at java.util.Scanner.nextInt(Scanner.java:2117)
        at java.util.Scanner.nextInt(Scanner.java:2076)
        at practise.ExceptionTest.getInputNumbers(ExceptionTest.java:30)
        at practise.ExceptionTest.add(ExceptionTest.java:48)
        ... 1 more    
    

    6 异常的信息

    异常实例的构造十分昂贵。这是由于在构造异常实例时,Java 虚拟机便需要生成该异常的栈轨迹(stack trace)。该操作会逐一访问当前线程的 Java 栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。

    image

    7 java虚拟机中异常处理

    程序源文件

    public class TryCatchFinally {
        private String tryBlock = "tryBlock";
        private String catchBlock = "catchBlock";
        private String finallyBlock = "finallyBlock";
    
        public String test() {
            try {
                System.out.println(tryBlock);
                //throw new RunTimeException();
                return tryBlock;
            } catch (Exception e) {
                System.out.println(catchBlock);
                return catchBlock;
            } finally {
                System.out.println(finallyBlock);
                //return finallyBlock;
            }
        }
    
        public static void main(String[] args) {
            System.out.println(new TryCatchFinally().test());
        }
    }
    

    程序编译后字节码指令

      public java.lang.String test();
        descriptor: ()Ljava/lang/String;
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=4, args_size=1
             0: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: aload_0
             4: getfield      #3                  // Field tryBlock:Ljava/lang/String;
             7: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            10: aload_0
            11: getfield      #3                  // Field tryBlock:Ljava/lang/String;
            14: astore_1
            15: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
            18: aload_0
            19: getfield      #7                  // Field finallyBlock:Ljava/lang/String;
            22: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            25: aload_1
            26: areturn
            27: astore_1
            28: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
            31: aload_0
            32: getfield      #5                  // Field catchBlock:Ljava/lang/String;
            35: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            38: aload_0
            39: getfield      #5                  // Field catchBlock:Ljava/lang/String;
            42: astore_2
            43: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
            46: aload_0
            47: getfield      #7                  // Field finallyBlock:Ljava/lang/String;
            50: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            53: aload_2
            54: areturn
            55: astore_3
            56: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
            59: aload_0
            60: getfield      #7                  // Field finallyBlock:Ljava/lang/String;
            63: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            66: aload_3
            67: athrow
          Exception table:
             from    to  target type
                 0    15    27   Class java/lang/Exception
                 0    15    55   any
                27    43    55   any
    
    7.1 Jvm如何实现异常捕获
    • 在编译生成的字节码中,每个方法都附带一个异常表。异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。这些指针的值是字节码索引(bytecode index,bci),用以定位字节码。

    • 其中,from 指针和 to 指针标示了该异常处理器所监控的范围,例如 try 代码块所覆盖的范围。target 指针则指向异常处理器的起始位置,例如 catch 代码块的起始位置。

    • 如此程序发生异常JVM会按照顺序遍历异常表中每一个声明的异常,首先从第一行开始,查看触发异常是否是type类指定的异常或其子类,如果是则从target处开始执行。如果不是则继续查看下一条。

     Exception table:
             from    to  target type
                 0    15    30   Class java/lang/Exception
                 0    15    61   any
                30    46    61   any
    
    • 其中Exception类型的异常是我们在应用程序源代码中定义的需要捕获的异常,该异常处理器所监控的范围为字节码指令0~15行。
    from    to  target  type
       0    15    30    Class java/lang/Exception
    
    //对应Java源文件代码
    try {
         System.out.println(tryBlock);
         return tryBlock;
    } catch (Exception e) {            
    
    • any类型表示任意类型异常,是JVM负责捕获的异常,当抛出程序没有捕获的异常时由any类型处理。
    from    to  target type
       0    15    61   any
      30    46    61   any
    
    7.2 程序的正常运行
    • 1 如下指令相当于执行try语句中System.out.println(tryBlock);
    0: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
    3: aload_0
    4: getfield      #3                  // Field tryBlock:Ljava/lang/String;
    7: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    
    //对应Java源文件代码
    try {
         System.out.println(tryBlock);
         ...
    }     
    
    
    • 2 如下指令相当于tryBlock变量保存到局部变量表中,待返回时使用
    10: aload_0
    11: getfield      #3                  // Field tryBlock:Ljava/lang/String;
    14: astore_1
    
    //对应Java源文件代码
    try {
         ...
         return tryBlock;
    }     
    
    • 3 如下指令指令相当于执行finally语句中System.out.println(finallyBlock)。【将finally代码块语句插入到try代码块返回前
    15: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
    18: aload_0
    19: getfield      #7                  // Field finallyBlock:Ljava/lang/String;
    22: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    
    //对应Java源文件代码
    finally {
        System.out.println(finallyBlock);
    }
    
    • 4 加载步骤2中保存的变量返回
    25: aload_1
    26: areturn
    
    //对应Java源文件代码
    try {
         ...
         return tryBlock;
    } 
    
    7.3 捕获异常处理

    按照异常表到发生Exception异常程序会跳到27行指令运行

    from    to  target   type
       0    15      27   Class java/lang/Exception
    
    • 1 如下指令指令相当于执行catch块语句中System.out.println(catchBlock);,并将异常对象e放入局部变量表的第一个。
    27: astore_1
    28: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
    31: aload_0
    32: getfield      #5                  // Field catchBlock:Ljava/lang/String;
    35: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    
    //对应Java源文件代码
    } catch (Exception e) {
        System.out.println(catchBlock);
        ...
    }
    
    • 2 如下指令相当于finallyBlock变量保存到局部变量表中第一个位置,待返回时使用
    38: aload_0
    39: getfield      #5                  // Field catchBlock:Ljava/lang/String;
    
    //对应Java源文件代码
    } catch (Exception e) {
        ...
        return catchBlock;
    }
    
    • 3 如下指令指令相当于执行finally语句中System.out.println(finallyBlock【将finally代码块语句插入到catch代码块返回前
    43: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
    46: aload_0
    47: getfield      #7                  // Field finallyBlock:Ljava/lang/String;
    50: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    
    //对应Java源文件代码
    finally {
        System.out.println(finallyBlock);
    }
    
    • 4 加载步骤2中保存的变量返回
    53: aload_2
    54: areturn
    
    //对应Java源文件代码
    } catch (Exception e) {
        ...
        return catchBlock;
    }
    
    7.4 未捕获异常处理

    如果try语句发生了catch中未定义的异常,或者catch代码块发生了异常则叫由JVM捕获执行,跳到61行指令运行

    from    to  target type
       0    15    61   any
      30    46    61   any
    
    • 1 如下指令指令相当于执行finally语句中System.out.println(finallyBlock,同时将异常对象放入局部变量表中。
    55: astore_3
    56: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
    59: aload_0
    60: getfield      #7                  // Field finallyBlock:Ljava/lang/String;
    63: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    
    • 2 读取步骤1中异常,向方法外抛出。
    66: aload_3
    67: athrow
    
    7.5 小结
    • 可以看出编译器将 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。

    • 包含三份 finally 代码块。其中,前两份分别位于 try 代码块和 catch 代码块的正常执行路径出口。最后一份在try 代码块触发的、未被 catch 代码块捕获的异常,以及 catch 代码块触发的异常时执行。

    image

    8 Java 7 的 Supressed 异常以及语法糖

    • Java 7 引入了 Supressed 异常来解决这个问题。这个新特性允许开发人员将一个异常附于另一个异常之上。因此,抛出的异常可以附带多个异常的信息。然而,Java 层面的 finally 代码块缺少指向所捕获异常的引用,所以这个新特性使用起来非常繁琐。为此,Java 7 专门构造了一个名为 try-with-resources的语法糖,
    • 在字节码层面自动使用 Supressed异常。当然,该语法糖的主要目的并不
    • 是使用 Supressed异常,而是精简资源打开关闭的用法。
    • 在 Java 7 之前,对于打开的资源,我们需要定义一个 finally 代码块,来确保该资源在正常或者异常执行状况下都能关闭。
    • 资源的关闭操作本身容易触发异常。因此,如果同时打开多个资源,那么每一个资源都要对应一个独立的 try-finally 代码块,以保证每个资源都能够关闭。这样一来,代码将会变得十分繁琐。
      FileInputStream in0 = null;
      FileInputStream in1 = null;
      FileInputStream in2 = null;
      ...
      try {
        in0 = new FileInputStream(new File("in0.txt"));
        ...
        try {
          in1 = new FileInputStream(new File("in1.txt"));
          ...
          try {
            in2 = new FileInputStream(new File("in2.txt"));
            ...
          } finally {
            if (in2 != null) in2.close();
          }
        } finally {
          if (in1 != null) in1.close();
        }
      } finally {
        if (in0 != null) in0.close();
      }
    

    Java 7 的 try-with-resources 语法糖,极大地简化了上述代码。程序可以在 try 关键字后声明并实例化实现了 AutoCloseable 接口的类,编译器将自动添加对应的 close() 操作。在声明多个 AutoCloseable 实例的情况下,编译生成的字节码类似于上面手工编写代码的编译结果。与手工代码相比,try-with-resources 还会使用 Supressed 异常的功能,来避免原异常“被消失”。

    public class Foo implements AutoCloseable {
      private final String name;
      public Foo(String name) { this.name = name; }
    
      @Override
      public void close() {
        throw new RuntimeException(name);
      }
    
      public static void main(String[] args) {
        try (Foo foo0 = new Foo("Foo0"); // try-with-resources
             Foo foo1 = new Foo("Foo1");
             Foo foo2 = new Foo("Foo2")) {
          throw new RuntimeException("Initial");
        }
      }
    }
    
    // 运行结果:
    Exception in thread "main" java.lang.RuntimeException: Initial
            at Foo.main(Foo.java:18)
            Suppressed: java.lang.RuntimeException: Foo2
                    at Foo.close(Foo.java:13)
                    at Foo.main(Foo.java:19)
            Suppressed: java.lang.RuntimeException: Foo1
                    at Foo.close(Foo.java:13)
                    at Foo.main(Foo.java:19)
            Suppressed: java.lang.RuntimeException: Foo0
                    at Foo.close(Foo.java:13)
                    at Foo.main(Foo.java:19)
    
    
    

    除了 try-with-resources 语法糖之外,Java 7 还支持在同一 catch 代码块中捕获多种异常。实际实现非常简单,生成多个异常表条目即可。

    // 在同一 catch 代码块中捕获多种异常
    try {
      ...
    } catch (SomeException | OtherException e) {
      ...
    }
    
    

    相关文章

      网友评论

          本文标题:Java虚拟机-异常的处理

          本文链接:https://www.haomeiwen.com/subject/iatedctx.html