bboyjing's blog

自己动手写JVM三十二【异常处理(一)】

异常处理是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虚拟机规范给了一个例子如下:

1
2
3
4
5
6
7
public class JVMSample {
void cantBeZero(int i) {
if (i == 0) {
throw new RuntimeException();
}
}
}

上述方法编译之后,产生的字节码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void cantBeZero(int);
Code:
// 把参数i推入操作数栈顶(局部变量表第1位)
0: iload_1
// 如果i不等于0,直接执行return指令
1: ifne 12
// 创建RuntimeException实例
4: new #2 // class java/lang/RuntimeException
// 赋值RuntimeException实例引用
7: dup
// 调用RuntimeException构造函数
8: invokespecial #3 // Method java/lang/RuntimeException."<init>":()V
// 抛出异常
11: athrow
// 方法返回
12: return

这里唯一陌生的指令是athrow,后面将实现。从字节码看,异常对象似乎也只是普通的对象,通过new指令创建,然后调用构造函数进行初始化。但实际上并非仅仅如此,查看RuntimeException的源码可以看出,构造函数中最终调用了Throwable的fillInStackTrace()方法,该方法中又调用了fillInStackTrace(0),这个方法是个本地方法,也就是说要想抛出异常,Java虚拟机必须实现这个本地方法。我们暂时给一个空的实现,后面会补全。实现代码位于/native/java/lang.Throwable.go文件中:

1
2
3
4
5
6
7
func init() {
native.Register("java/lang/Throwable", "fillInStackTrace", "(I)Ljava/lang/Throwable;", fillInStackTrace)
}
// private native Throwable fillInStackTrace(int dummy);
// (I)Ljava/lang/Throwable;
func fillInStackTrace(frame *rtdata.Frame) {}

异常处理表

异常处理是通过try-catch句实现的,还是参考Java虚拟机规范给出的例子,完整代码位于java项目的JVMSample.java中:

1
2
3
4
5
6
7
void catchOne() {
try {
tryItOut();
} catch (TestExc e) {
handleExc(e);
}
}

上面的方法编译后产生的字节码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void catchOne();
Code:
// 把局部变量表第0位(this引用)推入操作数栈顶
0: aload_0
// 调用tryItOut()方法
1: invokevirtual #4 // Method tryItOut:()V
// 如果没有抛出异常,直接跳转到return指令
4: goto 13
// 否则,异常对象引用在操作数栈顶,把它弹出并放到局部变量表第1位
7: astore_1
// 把this引用推入栈顶,作为handleExc()方法的参数0
8: aload_0
// 把异常对象引用推入栈顶,作为handleExc()方法的参数1
9: aload_1
// 调用handleExc()方法
10: invokevirtual #6 // Method handleExc:(Ljava/lang/RuntimeException;)V
// 方法返回
13: return

从字节码看来看,如果没有异常抛出,则会直接goto到return指令,方法正常返回。如果有异常抛出,将会查找方法的异常处理表,异常处理表是Code属性的一部分,它记录了方法是否有能力处理某种异常。回顾下方法的Code属性,下面只列出异常处理表部分:

1
2
3
4
5
6
7
8
9
10
Code_attribute {
...
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
...
}

异常处理表的每一项都包含3个信息:处理哪部分代码抛出的异常、哪类异常,以及异常处理代码在哪里。具体来说,start_pc和end_pc可以锁定一部分字节码,这部分字节码对应某个可能抛出异常的try{}代码快。catch_type是个索引,通过它可以从运行时常量池中查找一个类符号引用,解析后的这个类就是异常类,handler_pc指出负责处理异常的catch{}块在哪里。我们看下catchOne()方法的字节码:
jvmgo_47
下面我们先新建/rtdata/heap/exception_table.go文件,在其中定义ExceptionTable结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
type ExceptionTable []*ExceptionHandler
// 其中4个字段对应字节码中异常处理表的四个字段
type ExceptionHandler struct {
// try{}语句块的第一条指令
startPc int
// try{}语句块的下一条指令
endPc int
handlerPc int
// 若catchType为nil,则表示处理所有异常
catchType *ClassRef
}
// 创建异常处理表
func newExceptionTable(entries []*classfile.ExceptionTableEntry, cp *ConstantPool) ExceptionTable {
table := make([]*ExceptionHandler, len(entries))
for i, entry := range entries {
table[i] = &ExceptionHandler{
startPc: int(entry.StartPc()),
endPc: int(entry.EndPc()),
handlerPc: int(entry.HandlerPc()),
catchType: getCatchType(uint(entry.CatchType()), cp),
}
}
return table
}
func getCatchType(index uint, cp *ConstantPool) *ClassRef {
// index为0表示捕获所有异常
if index == 0 {
return nil // catch all
}
return cp.GetConstant(index).(*ClassRef)
}
// 查看异常处理表是否能处理异常
func (self ExceptionTable) findExceptionHandler(exClass *Class, pc int) *ExceptionHandler {
for _, handler := range self {
/*
处理try{}语句块中抛出的异常
try{]语句块包含startPc,但不包含endPc
*/
if pc >= handler.startPc && pc < handler.endPc {
if handler.catchType == nil {
return handler
}
catchClass := handler.catchType.ResolvedClass()
if catchClass == exClass || catchClass.IsSuperClassOf(exClass) {
return handler
}
}
}
return nil
}

然后再来修改/rtdata/heap/method.go文件,在里面增加异常处理表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Method struct {
...
// 异常处理表
exceptionTable ExceptionTable
}
func (self *Method) copyAttributes(cfMethod *classfile.MemberInfo) {
if codeAttr := cfMethod.CodeAttribute(); codeAttr != nil {
...
self.exceptionTable = newExceptionTable(codeAttr.ExceptionTable(),
self.class.constantPool)
}
}
// 查找异常处理表
func (self *Method) FindExceptionHandler(exClass *Class, pc int) int {
handler := self.exceptionTable.findExceptionHandler(exClass, pc)
// 如果找到对应的异常处理项,返回handlerPc字段
if handler != nil {
return handler.handlerPc
}
// 否则返回-1
return -1
}

异常处理表相关信息就实现到这里,就不讨论多个catch块、嵌套try-catch,以及finally子句等对应的字节码实现了。
下节接续。