异常处理是Java语言非常重要的一个语法,本章从Java虚拟机的角度来讨论异常是如何被抛出和处理的。
异常处理概述
在Java语言中,异常可以分为两类:Checked异常和Unchecked异常。Unchecked异常包括java.lang.RuntimeException、java.lang.Error以及它们的子类,其他异常都是Checked异常。所有异常都继承自java.lang.Throwable。如果一个方法有可能导致Checked异常抛出,则该方法要么需要捕获该异常并妥善处理,要么必须把该异常列咋自己的Throws子句中,否则无法通过编译。Unchecked异常没有这个限制。不过,Java虚拟机并没有这个规定,这只是Java语言的语法规则。
异常可以由Java虚拟机抛出,也可以由Java代码抛出。在代码中抛出和处理异常是由athrow指令和方法的异常处理表配合完成的,本章将重点学习这一点。
异常抛出
在Java代码中,异常是通过throw关键字抛出的,Java虚拟机规范给了一个例子如下:
上述方法编译之后,产生的字节码如下:
这里唯一陌生的指令是athrow,后面将实现。从字节码看,异常对象似乎也只是普通的对象,通过new指令创建,然后调用构造函数进行初始化。但实际上并非仅仅如此,查看RuntimeException的源码可以看出,构造函数中最终调用了Throwable的fillInStackTrace()方法,该方法中又调用了fillInStackTrace(0),这个方法是个本地方法,也就是说要想抛出异常,Java虚拟机必须实现这个本地方法。我们暂时给一个空的实现,后面会补全。实现代码位于/native/java/lang.Throwable.go文件中:
异常处理表
异常处理是通过try-catch句实现的,还是参考Java虚拟机规范给出的例子,完整代码位于java项目的JVMSample.java中:
上面的方法编译后产生的字节码如下:
从字节码看来看,如果没有异常抛出,则会直接goto到return指令,方法正常返回。如果有异常抛出,将会查找方法的异常处理表,异常处理表是Code属性的一部分,它记录了方法是否有能力处理某种异常。回顾下方法的Code属性,下面只列出异常处理表部分:
异常处理表的每一项都包含3个信息:处理哪部分代码抛出的异常、哪类异常,以及异常处理代码在哪里。具体来说,start_pc和end_pc可以锁定一部分字节码,这部分字节码对应某个可能抛出异常的try{}代码快。catch_type是个索引,通过它可以从运行时常量池中查找一个类符号引用,解析后的这个类就是异常类,handler_pc指出负责处理异常的catch{}块在哪里。我们看下catchOne()方法的字节码:
下面我们先新建/rtdata/heap/exception_table.go文件,在其中定义ExceptionTable结构体:
然后再来修改/rtdata/heap/method.go文件,在里面增加异常处理表:
异常处理表相关信息就实现到这里,就不讨论多个catch块、嵌套try-catch,以及finally子句等对应的字节码实现了。
下节接续。