各种编程语言都有自己的异常捕获、处理方式。在程序指令执行过程中,可能会发生可预知或不可预知的各种异常。通过异常捕获和处理可以记录相关异常日志,执行一些补救策略,让局部的错误不影响服务整体可用性。

那么Java的异常处理代码是如何被编译的?JVM又是如何捕获异常、执行异常处理逻辑的?

异常的抛出

在Java编程语言中,使用throw关键字来抛出异常

void cantBeZero(int i) throws TestExc {if (i == 0) {throw new TestExc();}
}

编译后:

0 iload_1
1 ifne 12
4 new #2 <com/supercoder/jvm/TestExc>
7 dup
8 invokespecial #3 <com/supercoder/jvm/TestExc.<init>>
11 athrow
12 return

athrow指令从操作数栈栈顶拿到一个TestExc实例的引用抛了出去。TestExc实例创建的过程不在这里讲了,在实例创建和方法调用的相关章节有详细讲解,可以找一找历史文章复习一下。

接着往下看,如何捕获、处理异常?

异常捕获——try-catch

在Java编程语言中,使用try将代码括起来,使用catch捕获指定的异常类型。try-catch支持有一个或多个catch子句,并且支持嵌套。

单个catch子句

void catchOne() {try {tryItOut();} catch (TestExc e) {handleExc(e);}
}

编译后:

Method void catchOne()
0 aload_0
1 invokevirtual #6
4 return
5 astore_1
6 aload_0
7 aload_1
8 invokevirtual #5
11 return
Exception table:
From To Target Type
0    4  5      Class TestExc

使用了try-catch之后,try代码块在编译时并没有特殊的改变,和没有try时一样。如果try代码块在执行过程中没有抛出任何异常,那么会和没有try时的表现一样:完成对tryItOut方法的调用,且catchOne方法正常返回。对handleExc方法的调用、以及catch子句中的内容都会作为正常的方法调用被编译。紧跟在try代码块指令后面的就是单个catch子句的指令。

由于catch子句的存在,编译器会生成一个异常表,[From,To)是一个左闭右开的区间,Target指向对应的异常处理器第一个指令位置。catchOne方法的异常表中有一个关联到TestExc实例的条目,catchOne方法的catch子句可以访问这个TestExc实例。在执行索引是0到4区间的指令时,如果有TestExc类型的值被抛出,控制将会转移到索引为5的指令,即catch子句代码块。如果抛出的值不是TestExc类型,catch子句是不能被执行的,这个值会被重新抛给catchOne方法的调用者。

多个catch子句

一个try也可以有多个catch子句。

void catchTwo() {try {tryItOut();} catch (TestExc1 e) {handleExc(e);} catch (TestExc2 e) {handleExc(e);}
}

编译后:

Method void catchTwo()
0 aload_0
1 invokevirtual #5
4 return
5 astore_1
6 aload_0
7 aload_1
8 invokevirtual #7
11 return
12 astore_1
13 aload_0
14 aload_1
15 invokevirtual #7
18 return
Exception table:
From To Target Type
0    4  5      Class TestExc1
0    4  12     Class TestExc2

一个try声明的多个catch子句在编译时也很简单,每一个catch子句编译后的指令序列都跟在另一个catch子句对应的指令后面,并且会添加一个条目到异常表中。

在索引0到4区间的try代码块运行期间,如果有一个值被抛出,该值的类型和一个或多个catch子句参数中的实例相匹配时,匹配的第一个catch子句会被选中,控制转移到这个catch子句的代码块。反之,如果抛出的值不能跟任何一个catch子句参数中的实例匹配。JVM会重新把这个值抛出给catchTwo方法的调用者,并且不会执行任何一个catch子句。

嵌套try-catch

嵌套try-catch的编译跟多个catch子句的编译非常相似。

void nestedCatch() {try {try {tryItOut();} catch (TestExc1 e) {handleExc1(e);}} catch (TestExc2 e) {handleExc2(e);}
}

编译后:

Method void nestedCatch()
0 aload_0
1 invokevirtual #8
4 return
5 astore_1
6 aload_0
7 aload_1
8 invokevirtual #7
11 return
12 astore_1
13 aload_0
14 aload_1
15 invokevirtual #6
Exception table:
From To Target Type
0    4  5      Class TestExc1
0    12  12     Class TestExc2

try-catch的嵌套只会在异常表中有所体现。每一个try-catch都对应异常表中的一个条目,它们From和To所表示的区间会出现交集。JVM并没有强制异常表条目的嵌套以及排序规则,不过,由于try-catch的构造是结构化的,编译器是可以对异常处理表中这些结构化数据进行排序的(例如可以按照Target从低到高排序)。排序之后,对于方法中抛出的任何异常,第一个与之匹配的异常处理程序就是最内层的匹配catch子句,内层不能匹配时会继续向外层尝试匹配。

异常处理中的兜底——finally

无论try代码块中是否有异常抛出,无论异常是否被catch住,finally代码块一定会被执行。

没有catch的try-finally

生成class文件的版本为50.0(即JDK1.6)及以下的编译器,在编译包含finally的异常处理代码时可能会同时使用两个特殊的指令:jsr(跳转到子程序)、ret(从子程序返回)。finally子句被作为方法的子程序被编译,类似异常处理器。

void tryFinally() {try {tryItOut();} finally {wrapItUp();}
}

编译后:

Method void tryFinally()
0 aload_0
1 invokevirtual #6
4 jsr 14
7 return
8 astore_1
9 jsr 14
12 aload_1
13 athrow
14 astore_2
15 aload_0
16 invokevirtual #5
19 ret 2
Exception table:
From    To      Target    Type
0          4        8     any

当没有catch,只有finally时,编译器也会在异常表中生成一个条目,Type是any,任何类型的异常都能够与之相匹配。当调用子程序的jsr指令被执行时,紧跟在jsr后指令的地址会作为returnAddress类型先被压入操作数栈,子程序代码会将这个地址存入局部变量表。子程序代码的尾部会有一个ret指令,该指令从局部变量表获取到返回地址并将控制转移到该地址处的指令。

完整的异常处理的结构——try-catch-finally

void tryCatchFinally() {try {tryItOut();} catch (TestExc e) {handleExc(e);} finally {wrapItUp();}
}

编译后:

Method void tryCatchFinally()
0 aload_0            //将局部变量表0处的this压入操作数栈(当前方法的调用者引用)
1 invokevirtual #4   //调用tryItOut方法
4 goto 16            //控制转移到index 16,执行jsr指令
7 astore_3           //[0,4)之间出现了异常,抛出的异常值存入局部变量表3的位置
8 aload_0            //this压栈
9 aload_3            //异常值压栈
10 invokevirtual #6  //调用handleExc方法
13 goto 16           //控制转移到index 16,执行jsr指令
16 jsr 26            //控制转移到index 26,执行finally子程序(会先把19压入操作数栈)
19 return         //执行finally子程序后,本次方法调用返回
20 astore_1          //TestExc类型不能与异常值类型匹配,异常值存入局部变量表1的位置
21 jsr 26            //控制转移到index 26,执行finally子程序(会先把24压入操作数栈)
24 aload_1           //将局部变量表1处的异常值压入操作数栈
25 athrow            //将异常重新抛出
26 astore_2          //从操作数栈弹出原始执行位置(19或24),存入局部变量表2的位置
27 aload_0           //局部变量表0处的this压入操作数栈
28 invokevirtual #5  //调用wrapItUp()
31 ret 2             //从局部变量表2的位置取出原始指令索引(19或24),控制转移到对应位置
Exception table:
From To  Target Type
0    4   7      Class TestExc
0    16  20     any

当try代码块正常完成时,会由goto指令将控制转移到索引16的位置,调用jsr指令去执行finally子程序。如果try代码块发生了异常且异常类型是TestExc,根据异常表的Target,控制会转移到索引7的位置,执行对应的异常处理逻辑,之后还会由goto指令将控制转移到索引16的位置,同样由jsr去调用finally子程序。若异常类型不能与catch子句的异常类型匹配,则会与any匹配,对应的异常处理逻辑会先调用finally子程序,然后将异常继续抛出给方法的调用者。

finally何时被执行?

有4种不同的方式,可以让finally子程序被调用。

  1. try子句正常执行完成,此时会在执行下一个指令前,通过jsr指令调用finally子程序。
  2. try子句内部的break或者continue子句将控制转移到try子句外时,会先通过jsr调用finally子程序。
  3. try子句中调用了return,会先通过jsr调用finally子程序。
  4. 引发一个异常。

50.0版本之后finally是如何被编译的?

50.0版本之后(从JDK1.7开始),不再使用jsr和ret指令执行finally指令的调用。取而代之的是一种非常简单暴力的方式:将finally子程序的指令序列编译到在每一个需要调用finally子程序的位置,这样做的代价就是字节码文件有所膨胀。

我们以JDK1.8为例,有如下代码:

public int test() {int i = 1;try {i++;return i;} catch (Exception e) {i++;return i;} finally {i = 10;}
}

编译后:

0 iconst_1
1 istore_1
2 iinc 1 by 1
5 iload_1
6 istore_2
7 bipush 10    //try正常执行,方法return之前执行finally逻辑
9 istore_1
10 iload_2
11 ireturn
12 astore_2    //异常与catch子句匹配,执行异常处理
13 iinc 1 by 1
16 iload_1
17 istore_3
18 bipush 10   //catch子句逻辑执行完,在return之前先执行finally逻辑
20 istore_1
21 iload_3
22 ireturn
23 astore 4
25 bipush 10   //try代码块抛出了异常,catch子句没有匹配上,先执行finally逻辑,然后将异常重新抛出给调用者
27 istore_1
28 aload 4
30 athrow
Exception table:
From To  Target Type
2    7   12     java/lang/Exception
2    7   23     any
12   18  23     any
23   25  23     any

从编译结果可以看到,没有出现jsr和ret指令,finally的指令序列重复出现在三个地方。

经典面试题

在本文最后一个例子中,test方法的局部变量:i,其值最终是多少?test方法的返回值又是多少?结合编译结果进行分析,欢迎留言讨论。

try catch异常后会执行后面的代码吗_JVM异常处理最强讲解相关推荐

  1. try catch异常后会执行后面的代码吗_Java的异常体系

    前言 大家在写代码的过程中,异常是不可能完全避免的,那么我们在尽可能规避错误的同时,还要注意如何去捕获和处理异常,此文带你详细理解Java的异常体系. 定义 Java异常指在程序运行时可能出现的一些错 ...

  2. java try catch 异常后还会继续执行吗

    java try catch 异常后还会继续执行吗? catch 中如果你没有再抛出异常 , 那么catch之后的代码是可以继续执行的 , 但是try中 , 报错的那一行代码之后 一直到try结束为止 ...

  3. SpringBoot配置在应用启动后立即执行某些方法代码案例

    springboot给我们提供了两种"开机启动"方式:ApplicationRunner和CommandLineRunner. 这两种方法提供的目的是为了满足,在项目启动的时候立刻 ...

  4. php 代码延迟执行,php和js编程中的延迟执行效果的代码

    php和js编程中的延迟执行效果的代码 php sleep(10); usleep(10); js里的 setInterval("方法", 100); PHP sleep() 函数 ...

  5. oracle 跳出内层循环,内层程序中发生异常后,不会继续执行外层程序的语句

    开发写了个存储过程需要我们审批,发现子程序中使用了异常处理语句, 通过以下实验说明这种写法的问题: SQL> create table test_number(test_id number); ...

  6. java实训 :异常(try-catch执行顺序与自定义异常)

    关键字: try:执行可能产生异常的代码 catch:捕获异常 finally:无论是否发生异常代码总能执行 throws:声明方法可能要抛出的各种异常 throw:手动抛出自定义异常 用 try-c ...

  7. java 异常后重试_java – 异常后自动重试的功能

    如果抛出一些异常,我已经使这个抽象类自动重试网络调用. >我在InterruptedException&之后注意不要重试 的UnknownHostException. >我重试了5 ...

  8. 为什么await()后会执行lock.unlock,await()时不就释放锁了吗

    为什么await()后会执行lock.unlock,await()时不就释放锁了吗 1 是的,释放锁是为了别的线程获得,是为了线程间的通信,是临时释放的,真正满足继续向下执行条件后,被唤醒后获得了锁, ...

  9. try、catch、finally的执行顺序

    有这样一段代码 : public class EmbededFinally { public static void main(String args[]){ int result; try { Sy ...

最新文章

  1. linux uvc 支持的设备,摄像头是否支持uvc
  2. Spark详解(十一):Spark运行架构原理分析
  3. factorymenu什么意思_MENU是什么意思
  4. linux2.4内核下载,升级到Linux 2.4内核
  5. redis学习(二) redis数据结构介绍以及常用命令
  6. java复习系列[3] - Java虚拟机
  7. php会不会被人工智能取代,人工智能真的会取代前端开发吗?
  8. ajaxFileUpload+struts2多文件上传(动态添加文件上传框)
  9. 剑指_4二维数组的查找(Python)
  10. ELF 文件数据分析: 全局变量
  11. switch芯片上的QoS,VLAN介绍
  12. 机器学习——神经网络(四):BP神经网络
  13. 百度地图、高德地图、腾讯地图三位一体地图定位开发
  14. Fast Global Registration
  15. css3绝对定位垂直居中,CSS3绝对定位自适应居中 - 米扑博客
  16. 安装步骤_Saber 2016 安装步骤
  17. 港科夜闻|香港科大商学院举办在线网络研讨会
  18. 寒假学习打卡第一篇文章-----numpy的学习
  19. 个人设置微信公众号自定义菜单的初级经验
  20. Caused by: java. io. IOException: Could not find resource com/kuang/dao/UserMapper.xml

热门文章

  1. 强化学习(十四) Actor-Critic
  2. (详细)Hibernate框架的搭建,Hibernate的CRUD操作(一)
  3. mysql 递归查出子级_Mysql选择递归获取具有多个级别的所有子级
  4. Spring 4 + Reactor Integration Example--转
  5. MYSQL存储过程中 使用变量 做表名--转
  6. 构建高性能服务(三)Java高性能缓冲设计 vs Disruptor vs LinkedBlockingQueue--转载
  7. java中的mmap实现--转
  8. 关于预付卡,您需要知道的事儿
  9. pipe()函数精解
  10. ubuntu 18.04 安装nodejs