Java虚拟机学习06 | JVM是如何处理异常的?

https://time.geekbang.org/column/article/12134

异常处理方式

1.抛出异常

显式抛出异常主体是应用程序,在程序中使用"throw"关键字手动抛出异常

隐式抛出异常指Java虚拟机在执行过程中,碰到无法继续执行的异常状态,自动抛出异常,常见的有数组越界异常

2.捕获异常

  • tyy-catch-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();
  }

  • try-with-resources

  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");
    }
  }

异常基本概念

Java语言规范中,所有的异常都是Throwable类或者它的子类的实例,Throwable有两个子类Error与Exception

  • Error:通俗理解,当出现Error,程序已经没救了,需要终止线程或者虚拟机
  • Exception:涵盖程序可能需要捕获并处理的异常
Throwable
         -Error
         -Exception
                   -RuntimeException
                   -OtherException

Exception有一个特殊的子类是RuntimeException,它与Error属于Java里的非检查异常(unchecked exception),其他的exception都属于检查时异常(checked exception).在Java语法中,所有的检查时异常都需要显式捕获或者在方法声明时用"throw"关键字标注. 通常情况下,程序的自定义异常都应该定义为检查时异常,方便利用Java编译器的编译时检查.

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

在生成栈轨迹时,Java 虚拟机不记录异常构造器以及填充栈帧的 Java 方法(Throwable.fillInStackTrace),直接从新建异常位置开始算起.此外,Java 虚拟机还会忽略标记为不可见的 Java 方法栈帧.

Java虚拟机是怎么捕捉异常的

在编译生成的字节码中,每个方法都附带一个异常表. 异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成. 这些指针的值是字节码索引(bytecode index,bci),用以定位字节码.

其中from和to指针标示异常处理器的监控范围(try代码块),target指向异常处理器的起始位置


public static void main(String[] args) {
  try {
    mayThrowException();
  } catch (Exception e) {
    e.printStackTrace();
  }
}
// 对应的 Java 字节码
public static void main(java.lang.String[]);
  Code:
    0: invokestatic mayThrowException:()V
    3: goto 11
    6: astore_1
    7: aload_1
    8: invokevirtual java.lang.Exception.printStackTrace
   11: return
  Exception table:
    from  to target type
      0   3   6  Class java/lang/Exception  // 异常表条目

在上述例子中,经过编译过,该方法的异常表拥有一个条目.其 from 指针和 to 指针分别为 0 和 3,代表它的监控范围从索引为 0 的字节码开始,到索引为 3 的字节码结束(不包括 3). 该条目的 target 指针是 6,代表这个异常处理器从索引为 6 的字节码开始. 条目的最后一列,代表该异常处理器所捕获的异常类型正是 Exception.

当程序触发异常时,Java 虚拟机会从上至下遍历异常表中的所有条目. 当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配. 如果匹配,Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码.如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧,并且在调用者(caller)中重复上述操作.在最坏情况下,Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表.

finally 代码块的编译比较复杂.当前版本 Java 编译器的做法,是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中.

针对异常执行路径,Java 编译器会生成一个或多个异常表条目,监控整个 try-catch 代码块,并且捕获所有种类的异常(在 javap 中以 any 指代).这些异常表条目的 target 指针将指向另一份复制的 finally 代码块.并且,在这个 finally 代码块的最后,Java 编译器会重新抛出所捕获的异常.

猜你喜欢

转载自blog.csdn.net/qq_34332035/article/details/87719888