bboyjing's blog

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

本节继续学习异常处理。

实现athrow指令

athrow属于引用类指令,在/instructions/references包下创建athrow.go文件,在其中定义athrow指令:

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
56
57
58
59
60
61
62
63
64
65
66
67
type ATHROW struct{ base.NoOperandsInstruction }
func (self *ATHROW) Execute(frame *rtdata.Frame) {
// athrow指令操作数为异常对象,从操作数栈中弹出
ex := frame.OperandStack().PopRef()
// 如果引用为空,抛出NullPointerException异常
if ex == nil {
panic("java.lang.NullPointerException")
}
thread := frame.Thread()
if !findAndGotoExceptionHandler(thread, ex) {
// 若没有找到异常处理代码块
handleUncaughtException(thread, ex)
}
}
// 查找并跳转到异常处理代码
func findAndGotoExceptionHandler(thread *rtdata.Thread, ex *heap.Object) bool {
for {
// 获取当前帧
frame := thread.CurrentFrame()
// 当前正在执行指令的地址为NextPC - 1
pc := frame.NextPC() - 1
// 查找异常处理表
handlerPC := frame.Method().FindExceptionHandler(ex.Class(), pc)
// 查找到异常处理代码块地址
if handlerPC > 0 {
// 获取当前操作数栈
stack := frame.OperandStack()
// 清空操作数栈
stack.Clear()
// 将异常对象引用入栈
stack.PushRef(ex)
// 设置执行下一条指令的地址为异常处理代码块的起始位置
frame.SetNextPC(handlerPC)
return true
}
// 遍历java虚拟机栈帧,若当前帧没有找到可用异常处理表,则弹出,执行上一帧
thread.PopFrame()
// 如果虚拟机栈空了,则退出循环
if thread.IsStackEmpty() {
break
}
}
return false
}
func handleUncaughtException(thread *rtdata.Thread, ex *heap.Object) {
// 清空JVM虚拟机栈
thread.ClearStack()
// 打印Java虚拟机栈信息
jMsg := ex.GetRefVar("detailMessage", "Ljava/lang/String;")
goMsg := heap.GoString(jMsg)
println(ex.Class().JavaName() + ": " + goMsg)
stes := reflect.ValueOf(ex.Extra())
for i := 0; i < stes.Len(); i++ {
ste := stes.Index(i).Interface().(interface {
String() string
})
println("\tat " + ste.String())
}
}

athrow指令的实现中有三个方法还没给出,下面列举下:

  1. 修改/rtdata/operand_stack.go文件,添加清空操作数栈的方法:

    1
    2
    3
    4
    5
    6
    func (self *OperandStack) Clear() {
    self.size = 0
    for i := range self.slots {
    self.slots[i].ref = nil
    }
    }
  2. 修改/rtdata/jvm_stack.go文件,添加清空JVM虚拟机栈的方法:

    1
    2
    3
    4
    5
    func (self *Stack) clear() {
    for !self.isEmpty() {
    self.pop()
    }
    }

3.修改/rtdata/thread.go文件,添加清空JVM虚拟机栈的方法:

1
2
3
func (self *Thread) ClearStack() {
self.stack.clear()
}

最后,修改下/instructions/factory.go文件,放开athrow指令的注释。

Java虚拟机栈信息

再回到/native/java/lang/Throwable.go文件,在其中定义StackTraceElement结构体:

1
2
3
4
5
6
7
8
9
10
11
// 该结构体用于记录Java虚拟机栈信息
type StackTraceElement struct {
// 类所在的文件名
fileName string
// 申明方法的类名
className string
// 方法名
methodName string
// 帧正在执行哪行代码
lineNumber int
}

在真正完成fillInStackTrace()方法之前,有些准备工作要坐下:

  1. 在/rtdata/jvm_stack.go文件中添加构造栈帧完整的栈帧的方法:

    1
    2
    3
    4
    5
    6
    7
    func (self *Stack) getFrames() []*Frame {
    frames := make([]*Frame, 0, self.size)
    for frame := self._top; frame != nil; frame = frame.lower {
    frames = append(frames, frame)
    }
    return frames
    }
  2. 在/rtdata/thread中添加上述调用:

    1
    2
    3
    4
    // 获取完整的Java虚拟机栈
    func (self *Thread) GetFrames() []*Frame {
    return self.stack.getFrames()
    }
  3. 修改/rtdata/heap/class.go文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    type Class struct {
    ...
    // 类所在的文件名
    sourceFile string
    }
    func (self *Class) SourceFile() string {
    return self.sourceFile
    }
    func newClass(cf *classfile.ClassFile) *Class {
    ...
    class.sourceFile = getSourceFile(cf)
    }
    // 要注意的是,并不是每个class文件都有源文件信息
    func getSourceFile(cf *classfile.ClassFile) string {
    if sfAttr := cf.SourceFileAttribute(); sfAttr != nil {
    return sfAttr.FileName()
    }
    return "Unknown" // todo
    }
  4. 修改/classfile/class_file.go文件,添加获取SourceFileAttribute的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func (self *ClassFile) SourceFileAttribute() *SourceFileAttribute {
    for _, attrInfo := range self.attributes {
    switch attrInfo.(type) {
    case *SourceFileAttribute:
    return attrInfo.(*SourceFileAttribute)
    }
    }
    return nil
    }
  5. 修改/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
    25
    type Method struct {
    ...
    // 行号表信息
    lineNumberTable *classfile.LineNumberTableAttribute
    }
    func (self *Method) copyAttributes(cfMethod *classfile.MemberInfo) {
    if codeAttr := cfMethod.CodeAttribute(); codeAttr != nil {
    ...
    self.lineNumberTable = codeAttr.LineNumberTableAttribute()
    }
    }
    // 和源文件一样,并不是每个方法都有行号表。
    func (self *Method) GetLineNumber(pc int) int {
    // 本地方法没有字节码,自然就没有行号表,返回-2
    if self.IsNative() {
    return -2
    }
    // 没有行号表,返回-1
    if self.lineNumberTable == nil {
    return -1
    }
    return self.lineNumberTable.GetLineNumber(pc)
    }
  6. 修改/classfile/attr_code.go文件,添加如下方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func (self *CodeAttribute) LineNumberTableAttribute() *LineNumberTableAttribute {
    for _, attrInfo := range self.attributes {
    switch attrInfo.(type) {
    case *LineNumberTableAttribute:
    return attrInfo.(*LineNumberTableAttribute)
    }
    }
    return nil
    }

最后可以来完成在真正完成fillInStackTrace()方法了:

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
56
57
58
59
60
61
62
63
// private native Throwable fillInStackTrace(int dummy);
// (I)Ljava/lang/Throwable;
func fillInStackTrace(frame *rtdata.Frame) {
// 获取局部变量表第0位,this引用
this := frame.LocalVars().GetThis()
// 将this引用入栈
frame.OperandStack().PushRef(this)
// 创建完整的堆栈打印信息
stes := createStackTraceElements(this, frame.Thread())
// 将完整的堆栈信息赋值到异常类实例(this)的extra字段中,athrow指令中打印了extra信息
this.SetExtra(stes)
}
// 该结构体用于记录Java虚拟机栈信息
type StackTraceElement struct {
// 类所在的文件名
fileName string
// 申明方法的类名
className string
// 方法名
methodName string
// 帧正在执行哪行代码
lineNumber int
}
func createStackTraceElements(tObj *heap.Object, thread *rtdata.Thread) []*StackTraceElement {
// 由于栈顶两帧正在执行fillInStackTrace(int)和fillInStackTrace(),所以这两帧也需要跳过
skip := distanceToObject(tObj.Class()) + 2
// 获取跳过的帧之后的完整的Java虚拟机栈
frames := thread.GetFrames()[skip:]
stes := make([]*StackTraceElement, len(frames))
for i, frame := range frames {
stes[i] = createStackTraceElement(frame)
}
return stes
}
// 计算需要跳过的帧(正在执行的异常类的构造函数)
func distanceToObject(class *heap.Class) int {
distance := 0
// 如果有超类,跳帧 + 1
for c := class.SuperClass(); c != nil; c = c.SuperClass() {
distance++
}
return distance
}
// 构造记录Java虚拟机栈信息的结构体
func createStackTraceElement(frame *rtdata.Frame) *StackTraceElement {
method := frame.Method()
class := method.Class()
return &StackTraceElement{
fileName: class.SourceFile(),
className: class.JavaName(),
methodName: method.Name(),
lineNumber: method.GetLineNumber(frame.NextPC() - 1),
}
}
func (self *StackTraceElement) String() string {
return fmt.Sprintf("%s.%s(%s:%d)",
self.className, self.methodName, self.fileName, self.lineNumber)
}

测试

在java项目中新建ParseIntTest.java,代码就不贴出来了,参照项目源码,下面给出测试结果:

1
2
3
4
5
6
7
8
cd /home/zhangjing/IdeaProjects/jvmgo/go
go install cn.didadu/jvmgo
./bin/jvmgo -classpath /home/zhangjing/IdeaProjects/jvmgo/java/target/classes cn.didadu.ParseIntTest
// 输出如下,由于无法捕获IndexOutOfBoundsException异常,打印出了完整的堆栈信息
java.lang.IndexOutOfBoundsException: no args!
at cn.didadu.ParseIntTest.bar(ParseIntTest.java:19)
at cn.didadu.ParseIntTest.foo(ParseIntTest.java:11)
at cn.didadu.ParseIntTest.main(ParseIntTest.java:6)

写在最后

本系列的学习就到此为止了。首先要感谢原作者不遗余力地写出本书供后来者学习,书中的知识对本人的提升很有帮助,建议有兴趣的Java从业者去买一本看看,支持正版。学到现在,最多也就只敢说是Java小学生,或许还够不上,学无止境,加油!