• 【JVM虚拟机】(9)-- JVM是如何处理异常的


    JVM是如何处理异常的

    上篇博客我们简单说过异常信息是存放在属性表集合中的Code属性表里,那么这篇博客就单独讲Code属性表中的exception_table。

    在讲之前我们先思考两个问题?

    1、为什么捕获异常会较大的性能消耗?

    2、为什么finally中的代码会永远执行?

    接下来会从JVM虚拟机的角度来解答这两个问题。

    一、概念

    1、JVM是如何捕获异常的?

    1、编译而成的字节码中,每个方法都附带一个异常表
    2、异常表中每一个条目代表一个异常处理器
    3、触发异常时,JVM会遍历异常表,比较触发异常的字节码的索引值是否在异常处理器的from指针到to指针的范围内。
    4、范围匹配后,会去比较异常类型和异常处理器中的type是否相同
    5、类型匹配后,会跳转到target指针所指向的字节码(catch代码块的开始位置)
    6、如果没有匹配到异常处理器,会弹出当前方法对应的Java栈帧,并对调用者重复上述操作。

    2、什么是异常表?

    1. 每个方法都附带一个异常表

    2. 异常表中每一个条目, 就是一个异常处理器

    异常表如下:

    3、什么是异常处理器?其组成部分有哪些?

    1、异常处理器由from指针、to指针、target指针,以及所捕获的异常类型所构成(type)。
    2、这些指针的数值就是字节码的索引(bytecode index, bci),可以直接去定位字节码。
    3、from指针和to指针,标识了该异常处理器所监控的返回
    4、target指针,指向异常处理器的起始位置。如catch代码块的起始位置
    5、type:捕获的异常类型,如Exception
    

    4、如果在方法的异常表中没有匹配到异常处理器,会怎么样?

    1、会弹出当前方法对应的Java栈帧
    2、在调用者上重复异常匹配的流程。
    3、最坏情况下,JVM需要编译当前线程Java栈上所有方法的异常表
    

    二、代码演示

    1、try-catch

    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  // 异常表条目
    

    上面Code中的字节码自己也没有仔细研究过,我们可以具体看下面的Exception table表,来进行分析。

    1、from和to: 指是try和catch之间的代码的索引位置。from=0,to=3,代表从字节索引0的位置到3(不包括3)。

    2、target : 代表catch后代码运行的起始位置。

    3、type : 指的是异常类型,这里是指Exception异常。

    当程序触发异常时,java虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常

    和该条目想要捕获的异常是否匹配。如果匹配,Java 虚拟机会将控制流转移至该条目target 指针指向的字节码。

    如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的Java 栈帧,并且在调用者(caller)中重复上述操作。在最坏情况下,

    Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表。

    2、try-catch-finally

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

    代码示例

    public static void XiaoXiao() {
       try {
           dada();
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           System.out.println("Finally");
       }
    }
    //通过javap 反编译
    public static void XiaoXiao();
        Code:
           0: invokestatic  #3                  // Method dada:()V
           3: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
           6: ldc           #7                  // String Finally
           8: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
          11: goto          41
          14: astore_0
          15: aload_0
          16: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
          19: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
          22: ldc           #7                  // String Finally
          24: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
          27: goto          41
          30: astore_1
          31: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
          34: ldc           #7                  // String Finally
          36: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
          39: aload_1
          40: athrow
          41: return
        Exception table:
           from    to  target type
               0     3    14   Class java/lang/Exception
               0     3    30   any
              14    19    30   any
    

    和之前有所不同,这次

    1、异常表中,有三条数据,而我们仅仅捕获了一个Exception
    2、异常表的后两个item的type为any

    上面的三条异常表item的意思为

    1、如果0到3之间,发生了Exception类型的异常,调用14位置的异常处理者。

    2、 如果0到3之间,无论发生什么异常,都调用30位置的处理者。

    3、 如果14到19之间(即catch部分),不论发生什么异常,都调用30位置的处理者。


    `问题`:通过上面那幅图和javap反编译代码就可以很好的解释为什么finally里面的代码永远会执行?

    原因:因为当前版本Java编译器的做法,是复制finally代码块的内容,分别放到所有正常执行路径,以及异常执行路径的出口中

    这三份finally代码块都放在什么位置:

    第一份位于try代码后 : 若果try中代码正常执行,没有异常那么finally代码就在这里执行。
    第二份位于catch代码后 : 如果try中有异常同时被catch捕获,那么finally代码就在这里执行。
    第三份位于异常执行路径 : 如果如果try中有异常但没有被catch捕获,或者catch又抛异常,那么就执行最终的finally代码。


    问题 :为什么捕获异常会较大的性能消耗?

    因为构造异常的实例比较耗性能。这从代码层面很难理解,不过站在JVM的角度来看就简单了,因为JVM在构造异常实例时需要生成该异常的栈轨迹。这个操作会逐一访问当前

    线程的栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息。虽然具体不清楚JVM的实现细节,但

    是看描述这件事情也是比较费时费力的。

    参考

    深入拆解 Java 虚拟机(郑雨迪)



    只要自己变优秀了,其他的事情才会跟着好起来(少将7)
    
  • 相关阅读:
    下载文件
    Cookie方法
    阿拉伯数字转大写
    格式化日期
    正向代理与反向代理
    get post 区别
    gulp
    什么是javascript中的同步&&异步?
    懒加载
    js操作dom时发生了什么?
  • 原文地址:https://www.cnblogs.com/qdhxhz/p/10765839.html
Copyright © 2020-2023  润新知