编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 1.1String深入理解原理
  • 1.2浮点型数据深入研究
  • 1.3数据装箱和拆箱原理
  • 1.4泛型由来和设计思想
  • 1.5加密和解密设计和原理
  • 2.1面向对象设计思想
  • 2.2抽象类和接口设计
  • 2.3封装和继承设计思想
  • 2.4复用和组合设计思想
  • 2.5对象和引用设计思想
  • 3.1IO流设计思想和原理
  • 3.2为何设计序列化数据
  • 3.3各种拷贝数据比较
  • 3.4高效文件读写的原理
  • 4.1反射性能探索和优化
  • 4.2为何要设计注解思想
  • 4.3动态代理的设计思想
  • 4.4SPI机制设计的思想
  • 4.5异常设计和捕获原理
  • 4.6虚拟机如何处理异常
  • 4.7四种引用设计思想
  • 5.1线程的前世今生探索
  • 5.2线程通信的设计思想
  • 5.3线程监控和Debug设计
  • 5.4线程和JVM之间联系
  • 5.5线程池使用技巧介绍
  • 5.6线程池设计核心原理
  • 5.7线程如何最大优化
  • 6.1多线程并发经典案例
  • 6.2并发安全前世今生
  • 6.3线程安全如何保证
  • 6.4变量的线程安全探索
  • 6.5并发上下文切换原理
  • 6.6理解CAS设计和由来
  • 6.7协程设计思想和原理
  • 6.8事物并发模型解读
  • 6.9并发设计模型研究
  • 6.10并发编程数据一致性
  • 6.11锁问题的定位和修复
  • 6.12多线程如何性能调优
  • 7.1类的加载过程和原理
  • 7.2对象布局设计的原理
  • 7.3双亲委派机制设计思想
  • 7.5代码攻击和安全防护
  • 7.6设计动态生成Java类

4.6虚拟机如何处理异常

目录介绍

  • 00.该文总纲的阅览
  • 01.JVM处理异常
  • 02.捕获异常涉及三块
  • 03.虚拟机如何捕获异常
  • 04.看一个实践案例
  • 05.Suppressed异常以及语法糖
  • 06.异常栈轨迹说明

00.该文总纲的阅览

  • 异常实例的构造十分昂贵
    • 由于在构造异常实例时,JVM 需要生成该异常的栈轨迹,该操作逐一访问当前线程的 Java 栈桢,并且记录下各种调试信息,包括栈桢所指向方法的名字、方法所在的类名以及方法在源代码中的位置等信息。
  • JVM 捕获异常需要异常表
    • 每个方法都有一个异常表,异常表中的每一个条目都代表一个异常处理器,并且由 from、to、target 指针及其异常类型所构成。form-to 其实就是 try 块,而 target 就是 catch 的起始位置。
    • 当程序触发异常时,JVM 会检测触发异常的字节码的索引值落到哪个异常表的 from-to 范围内,然后再判断异常类型是否匹配,匹配就开始执行 target 处字节码处理该异常。
  • 最后是finally代码块的编译
    • finally 代码块一定会运行的(除非虚拟机退出了)。那么它是如何实现的呢?其实是一个比较笨的办法,当前 JVM 的做法是,复制 finally 代码块的内容,分别放在所有可能的执行路径的出口中。

01.JVM处理异常

1.1 JVM默认是如何处理异常的

  • main函数收到这个问题时,有两种处理方式:
    • a:自己将该问题处理,然后继续运行
    • b:自己没有针对的处理方式,只有交给调用main的jvm来处理
  • jvm有一个默认的异常处理机制,就将该异常进行处理.
  • 并将该异常的名称,异常的信息.异常出现的位置打印在了控制台上,同时将程序停止运行

1.2 异常分类

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

1.3 异常构造昂贵

  • 这是由于在构造异常实例时,Java虚拟机便需要生成该异常的栈轨迹(stack trace)。
    • 该操作会逐一访问当前线程的Java栈帧,并且记录下各种调试信息,包括栈帧所指向的方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。
  • 当然,在生成栈轨迹时,Java虚拟机会忽略掉异常构造器以及填充栈帧的Java方法(Throwable.fillInStackTrace),直接从新建异常位置开始算起。此外,Java虚拟机还会忽略标记为不可见的Java方法栈帧。
  • 既然异常实例的构造十分昂贵,我们是否可以缓存异常实例,在需要用到的时候直接抛出呢?
    • 从语法角度上来看,这是允许的。然而,该异常对应的栈轨迹并非throw语句的位置,而是新建异常的位置。因此,这种做法可能会误导开发人员,使其定位到错误的位置。这也是为什么在实践中,我们往往选择抛出新建异常实例的原因。

02.捕获异常涉及三块

2.1 捕获异常三块

  • 捕获异常这涉及了如下三种代码块:
  • try代码块:用来标记需要进行异常监控的代码。
  • catch代码块:跟在try代码块之后,用来捕获在try代码块中触发的某种指定类型的异常。除了声明所捕获异常的类型之外,catch代码块还定义了针对该异常类型的异常处理器。在Java中,try代码块后面可以跟着多个catch代码块,来捕获不同类型的异常。Java虚拟机会从上至下匹配异常处理器。因此,前面的catch代码块所捕获的异常类型不能覆盖后边的,否则编译器会报错。
  • finally代码块:跟在try代码块和catch代码块之后,用来声明一段必定运行的代码。它的设计初衷是为了避免跳过某些关键的清理代码,例如关闭已打开的系统资源。

2.2 代码块异常分析

  • 在程序正常执行的情况下,这段代码会在try代码块之后运行。否则,也就是try代码块触发异常的情况下,如果该异常没有被捕获,finally代码块会直接执行,并且在运行之后重新抛出该异常。
  • 如果该异常被catch代码块捕获,finally代码块则在catch代码块之后运行。在某些不幸的情况下,catch代码同样也触发了异常,那么finally代码块同样会运行,并会抛出catch代码块触发的异常。在某些极端不幸的情况下,finally代码块也触发了异常,那么只好中断当前finally代码块的执行,并往外抛异常。

03.虚拟机如何捕获异常

  • 在编译生成的字节码中,每个方法都附带一个异常表。
    • 异常表中的每一个条目都代表一个异常处理器,并且由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      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代码块所有正常执行路径以及异常执行路径的出口中。

04.看一个实践案例

  • 针对异常执行路径,Java编译器会生成一个或多个异常表条目,监控整个try-catch代码块,并且捕获所有种类的异常(在javap中以any指代)。
    • 这些异常表条目的target指针将指向另一份复制的finally代码块。并且,在这个finally代码块的最后,Java编译器会重新抛出所捕获的异常。
    • image
      image
  • 我们可以使用javap工具来查看下面这段包含了try-catch-finally代码块的编译结果。
    • 为了更好地区分每个代码块,我们定义了四个实例字段:tryBlock、catchBlock、finallyBlock以及methodExit,并且仅在对应的代码块中访问这些字段。
  • 测试案例代码
    public class Foo{
    	private int tryBlock;
    	private int catchBlock;
    	private int finallyBlock;
    	private int methodExit;
     
    	public void test(){
    		try{
    			tryBlock = 0;
    		} catch(Exception e){
    			catchBlock = 1;
    		} finally{
    			finallyBlock = 2;
    		}
    		methodExit = 3;
    	}
    }
  • 然后编译并查看其字节码
    javac Foo.java
    javap -c Foo
    Compiled from "Foo.java"
    public class Foo {
      public Foo();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
     
      public void test();
        Code:
           0: aload_0
           1: iconst_0
           2: putfield      #2                  // Field tryBlock:I
           5: aload_0
           6: iconst_2
           7: putfield      #3                  // Field finallyBlock:I
          10: goto          35
          13: astore_1
          14: aload_0
          15: iconst_1
          16: putfield      #5                  // Field catchBlock:I
          19: aload_0
          20: iconst_2
          21: putfield      #3                  // Field finallyBlock:I
          24: goto          35
          27: astore_2
          28: aload_0
          29: iconst_2
          30: putfield      #3                  // Field finallyBlock:I
          33: aload_2
          34: athrow
          35: aload_0
          36: iconst_3
          37: putfield      #6                  // Field methodExit:I
          40: return
        Exception table:
           from    to  target type
               0     5    13   Class java/lang/Exception
               0     5    27   any
              13    19    27   any
    }
  • 可以看到,便以结果包含三份finally代码块。
    • 其中,前两份分别位于try代码块和catch代码块的正常执行路径出口。最后一份则作为异常处理器,监控try代码块以及catch代码块。它将捕获try代码块触发的、未被catch代码块捕获的异常,以及catch代码块触发的异常。
    • 如果catch代码块捕获了异常,并且触发了另一个异常,那么finally捕获并且重拋的异常是哪个呢?答案是后者。也就是说原本的异常便会被忽略掉,这对于代码调试来说十分不利。

05.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");
      		}
      	}
      }
  • 运行结果如下:
    javac Foo.java 
    java Foo
    Exception in thread "main" java.lang.RuntimeException: Initial
    	at Foo.main(Foo.java:16)
    	Suppressed: java.lang.RuntimeException: Foo2
    		at Foo.close(Foo.java:9)
    		at Foo.main(Foo.java:17)
    	Suppressed: java.lang.RuntimeException: Foo1
    		at Foo.close(Foo.java:9)
    		at Foo.main(Foo.java:17)
    	Suppressed: java.lang.RuntimeException: Foo0
    		at Foo.close(Foo.java:9)
    		at Foo.main(Foo.java:17)
  • 除了 try-with-resources语法糖之外,Java 7 还支持在同一catch代码块中捕获多种异常。实际实现非常简单,生成多个异常表条目即可。
    //在同一 catch 代码块中捕获多种异常
    try {
        ...
    } catch (SomeException | OtherException e) {
        ...
    }
  • 编写Java源代码
    public class Foo{
    	private int tryBlock;
    	private int catchBlock;
    	private int finallyBlock;
    	private int methodExit;
     
    	public void test(){
    		for (int i = 0; i < 100; i++){
    			try{
    				tryBlock = 0;
    				if(i < 50){
    					continue;
    				}else if(i < 80){
    					break;
    				}else {
    					return;
    				}
    			} catch(Exception e){
    				catchBlock = 1;
    			} finally{
    				finallyBlock = 2;
    			}
    		}
    		methodExit = 3;
    	}
    }
  • 编译并查看其字节码
    javac Foo.java 
    javap -c Foo
    Compiled from "Foo.java"
    public class Foo {
      public Foo();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
     
      public void test();
        Code:
           0: iconst_0
           1: istore_1
           2: iload_1
           3: bipush        100
           5: if_icmpge     75
           8: aload_0
           9: iconst_0
          10: putfield      #2                  // Field tryBlock:I
          13: iload_1
          14: bipush        50
          16: if_icmpge     27
          19: aload_0
          20: iconst_2
          21: putfield      #3                  // Field finallyBlock:I
          24: goto          69
          27: iload_1
          28: bipush        80
          30: if_icmpge     41
          33: aload_0
          34: iconst_2
          35: putfield      #3                  // Field finallyBlock:I
          38: goto          75
          41: aload_0
          42: iconst_2
          43: putfield      #3                  // Field finallyBlock:I
          46: return
          47: astore_2
          48: aload_0
          49: iconst_1
          50: putfield      #5                  // Field catchBlock:I
          53: aload_0
          54: iconst_2
          55: putfield      #3                  // Field finallyBlock:I
          58: goto          69
          61: astore_3
          62: aload_0
          63: iconst_2
          64: putfield      #3                  // Field finallyBlock:I
          67: aload_3
          68: athrow
          69: iinc          1, 1
          72: goto          2
          75: aload_0
          76: iconst_3
          77: putfield      #6                  // Field methodExit:I
          80: return
        Exception table:
           from    to  target type
               8    19    47   Class java/lang/Exception
              27    33    47   Class java/lang/Exception
               8    19    61   any
              27    33    61   any
              47    53    61   any
    }
  • 得出结论
    • 由此可见,finally代码块被拷贝到了if语句的每个分支之后(如果分支中有return语句,则在该语句之前)

06.异常栈轨迹说明

6.1 看崩溃信息

  • 首先看一个异常崩溃信息
    java.lang.RuntimeException: Unable to destroy activity {com.didi.global.rider/com.didi.rider.business.main.RiderMainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'com.google.android.libraries.navigation.internal.eg.f com.google.android.libraries.navigation.internal.eg.h.n()' on a null object reference
        at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:4491)
        at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:4509)
        at android.app.servertransaction.DestroyActivityItem.execute(DestroyActivityItem.java:39)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:145)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1836)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6704)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:916)

6.2 栈轨迹如何形成

  • 构造异常实例时,Java虚拟机便需要生成该异常的栈轨迹(stack trace)。
    • 该操作会逐一访问当前线程的Java栈帧,并且记录下各种调试信息,包括栈帧所指向的方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。
贡献者: yangchong211
上一篇
4.5异常设计和捕获原理
下一篇
4.7四种引用设计思想