手写JVM(二十九)-异常处理

代码、内容参考来自于张秀宏大佬的自己动手写Java虚拟机 (Java核心技术系列)以及尚硅谷宋红康:JVM全套教程。

异常处理是Java语言非常重要的一个语法。

1.异常处理概述

在Java语言中,异常可以分为两类:Checked异常和Unchecked异常。

  • Unchecked异常包括java.lang.RuntimeException、java.lang.Error以及它们的子类,其他异常都是Checked异常。
  • 所有异常都最终继承自java.lang.Throwable。如果一个方法有可能导致Checked异常抛出,则该方法要么需要捕获该异常并妥善处理,要么必须把该异常列在自己的throws子句中,否则无法通过编译。Unchecked异常没有这个限制。

请注意,Java虚拟机规范并没有这个规定,这只是Java语言的语法规则。异常可以由Java虚拟机抛出,也可以由Java代码抛出。

  • 当Java虚拟机在运行过程中遇到比较严重的问题时,会抛出java.lang.Error的某个子类,如StackOverflowError、OutOfMemoryError等。程序一般无法从这种异常里恢复,所以在代码中通常也不必关心这类异常。
  • 一部分指令在执行过程中会导致Java虚拟机抛出java.lang.RuntimeException的某个子类,如NullPointerExceptionIndexOutOfBoundsException等。这类异常一般是代码中的bug导致的,需要格外注意。

 

2.异常抛出

在Java代码中,异常是通过throw关键字抛出的。

Java虚拟机规范的3.12节给了一个例子:

查看java.lang.Exception或RuntimeException的源代码就可以知道,它们的构造函数都调用了超类java.lang.Throwable的构造函数。Throwable的构造函数又调用了fillInStackTrace()方法记录Java虚拟机栈信息,这个方法的代码如下:

fillInStackTrace()借助另外一个本地方法也就是重载后的fillInStackTrace(int)方法才能访问Java虚拟机栈,代码如下:

 

之后再实现他,先给它一个空的实现。

在\native\java\lang目录下创建Throwable.go文件,在其中注册fillInStackTrace(int)方法,代码如下:

package lang

import (
    "jvmgo/ch10/native"
    "jvmgo/ch10/rtda"
)

const jlThrowable = "java/lang/Throwable"

func init() {
    native.Register(jlThrowable, "fillInStackTrace", "(I)Ljava/lang/Throwable;", fillInStackTrace)
}

// private native Throwable fillInStackTrace(int dummy);
// (I)Ljava/lang/Throwable;
func fillInStackTrace(frame *rtda.Frame) {
}

 

3.异常处理表

1、处理异常
在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(早期使用jsr、ret指令) ,而是采用异常表来完成的。
2、异常表
如果一个方法定义了一个try-catch 或者try-finally的异常处理,就会创建一个异常表。它包含了每个异常处理或者finally块的信息,异常表保存了每个异常处理信息。比如:
  • 起始位置
  • 结束位置
  • 程序计数器记录的代码处理的偏移地址
  • 被捕获的异常类在常量池中的索引
当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。
不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行,在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在return前,它直接跳到finally块来完成目标

举例:

public void tryCatch(){
    try{
        File file = new File("d:/hello.txt");
        FileInputStream fis = new FileInputStream(file);

        String info = "hello!";
    }catch (FileNotFoundException e) {
        e.printStackTrace();
    }
    catch (RuntimeException e){
        e.printStackTrace();
    }
}

可以看到,到 goto 38 (+16),会跳转出,那22到38的代码呢?

异常表:

如图:如果在Start PC 到 End PC 中出现异常就跳转到 对应的Handler PC

注意:25和33,会将异常类的实例对象地址放到局部变量表。

 

思考:

//思考:如下方法返回结果为多少?
public static String func() {
    String str = "hello";
    try{
        return str;
    }
    finally{
        str = "崩坏三天下第一";
    }
}

public static void main(String[] args) {

    System.out.println(func());//hello
}

如图,在finally前已经return了

 

异常处理表是Code属性的一部分,它记录了方法是否有能力处理某种异常。回顾一下方法的Code属性,它的结构如下:


异常处理表的每一项都包含3个信息:处理哪部分代码抛出的异常、哪类异常,以及异常处理代码在哪里。具体来说,start_pc和end_pc可以锁定一部分字节码,这部分字节码对应某个可能抛出异常的try{}代码块。catch_type是个索引,通过它可以从运行时常量池中查到一个类符号引用,解析后的类是个异常类,假定这个类是X。如果位于start_pc和end_pc之间的指令抛出异常x,且x是X(或者
X的子类)的实例,handler_pc就指出负责异常处理的catch{}块在哪里。

当上面的方法通过athrow指令抛出异常时,Java虚拟机首先会查找该方法的异常处理表,看它能否处理该异常。如果能,则跳转到相应的字节码开始异常处理。假设该方法无法处理异常,Java虚拟机会进一步查看它的调用者的异常处理表。假设也无法处理异常,Java虚拟机会继续查找它的调用者的调用者的异常处理表。这个过程会一直继续下去,直到找到某个异常处理项,或者到达Java虚拟机栈的底部。

 

把这部分逻辑放在athrow指令中。

在\rtda\heap目录下创建exception_table.go文件,在其中定义ExceptionTable类型,代码如下:

package heap

import "jvmgo/ch10/classfile"

// 别名
type ExceptionTable []*ExceptionHandler

type ExceptionHandler struct {
    startPc int
    endPc int
    handlerPc int
    // 若catchType为nil,则表示处理所有异常
    catchType *ClassRef
}

// 把class文件中的异常处理表转换成ExceptionTable类型,创建异常处理表
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 {
        // jvms: The start_pc is inclusive and end_pc is exclusive
        //处理try{}语句块中抛出的异常 try{]语句块包含startPc,但不包含endPc
        if pc >= handler.startPc && pc < handler.endPc {
            if handler.catchType == nil {
                return handler
            }
            //通过它从运行时常量池中查到一个类符号引用,解析后的类是个异常类
            catchClass := handler.catchType.ResolvedClass()
            //如果catchClass是exClass(或者exClass的子类)的实例
            if catchClass == exClass || catchClass.IsSuperClassOf(exClass) {
                return handler
            }
        }
    }
    return nil
}

解释一下上面的代码:
ExceptionTable只是[]*ExceptionHandler的别名

ExceptionHandler的定义

newExceptionTable()函数把class文件中的异常处理表转换成ExceptionTable类型。有一点需要特别说明:异常处理项的catchType有可能是0。我们知道0是无效的常量池索引,但是在这里0并非表示catch-none,而是表示catch-all,所以赋值后,若catchType为nil,则表示处理所有异常
getCatchType()函数从运行时常量池中查找类符号引用
以从运行时常量池中查到一个类符号引用,解析后的类是个异常类,假定这个类是X。如果位于start_pc和end_pc之间的指令抛出异常x,且x是X(或者X的子类)的实例,handler_pc就指出负责异常处理的catch{}块在哪里。注意两点。第一,startPc给出的是try{}语句块的第一条指令,endPc给出的则是try{}语句块的下一条指令。第二,如果catchType是nil(在class文件中是0,getCatchType()函数赋值为nil),表示可以处理所有异常,这是用来实现finally子句的。

 

下面修改Method结构体,在里面增加异常处理表。打开ch10\rtda\heap\method.go文件,给Method结构体添加exceptionTable字段,代码如下:

type Method struct {
    ClassMember
    maxStack uint
    maxLocals uint
    code []byte
    exceptionTable ExceptionTable
    argSlotCount uint
}

然后修改copyAttributes()方法,从Code属性中复制异常处理表,代码如下:

func (self *Method) copyAttributes(cfMethod *classfile.MemberInfo) {
    if codeAttr := cfMethod.CodeAttribute(); codeAttr != nil {
        self.maxStack = codeAttr.MaxStack()
        self.maxLocals = codeAttr.MaxLocals()
        self.code = codeAttr.Code()
        self.exceptionTable = newExceptionTable(codeAttr.ExceptionTable(),
        self.class.constantPool)
    }
}

继续编辑method.go文件,给Method结构体添加FindExceptionHandler()方法,FindExceptionHandler()方法调用ExceptionTable.findExceptionHandler()方法搜索异常处理表,如果能够找到对应的异常处理项,则返回它的handlerPc字段,否则返回–1。代码如下:

func (self *Method) FindExceptionHandler(exClass *Class, pc int) int {
    handler := self.exceptionTable.findExceptionHandler(exClass, pc)
    if handler != nil {
        return handler.handlerPc
    }
    return -1
}

 

4.实现athrow指令

1、抛出异常指令:

(1)athrow指令

在Java程序中显示抛出异常的操作(throw语句)都是由athrow指令来实现。除了使用throw语句显示抛出异常情况之外, JVM规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在之前介绍的整数运算时,当除数为零时,虚拟机会在 idiv或 ldiv指令中抛出ArithmeticException异常。

(2)注意

正常情况下,操作数栈的压入弹出都是一条条指令完成的。唯一的例外情况是在抛异常时, Java虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上。

异常及异常的处理:

过程一:异常对象的生成过程 —>throw(手动 / 自动)指令 athrow

过程二:异常的处理:抓抛模型。 try-catch-finally     使用异常表

清晰明了,不再赘述。

public void throwZero(int i){
    if(i == 0){
        throw new RuntimeException("参数值为0");
    }
}

 

对比一下,下面的两个代码

public void throwOne(int i){
    if(i == 1){
        throw new RuntimeException("参数值为1");
    }
}

public void throwOne(int i) throws RuntimeException,IOException{
    if(i == 1){
        throw new RuntimeException("参数值为1");
    }
}


没什么变化,但多了Exceptions属性

注意一下,JVM规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。这样是看不到对应的字节码指令。

public void throwArithmetic() {
    int i = 10;
    int j = i / 0;
    System.out.println(j);
}

我们这里实现athrow指令

整体代码

修改/rtda/operand_stack.go文件,添加清空操作数栈的方法,OperandStack结构体的Clear()方法:
func (self *OperandStack) Clear() {
    self.size = 0
    for i := range self.slots {
        self.slots[i].ref = nil
    }
}
修改/rtda/thread.go文件,添加清空JVM虚拟机栈的方法,Thread结构体的ClearStack()方法,它调用了Stack结构体的clear()方法,代码如下:
func (self *Thread) ClearStack() {
    self.stack.clear()
}
修改/rtda/jvm_stack.go文件,添加清空JVM虚拟机栈的方法,:
func (self *Stack) clear() {
    for !self.isEmpty() {
        self.pop()
    }
}

athrow属于引用类指令,在\instructions\references目录下创建athrow.go文件,在其中定义athrow指令,代码如下:

package references

import (
    "jvmgo/ch10/instructions/base"
    "jvmgo/ch10/rtda"
    "jvmgo/ch10/rtda/heap"
    "reflect"
)

// Throw exception or error
type ATHROW struct{ base.NoOperandsInstruction }

func (self *ATHROW) Execute(frame *rtda.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 *rtda.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
}

// todo
func handleUncaughtException(thread *rtda.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指令的操作数是一个异常对象引用,从操作数栈弹出。先从操作数栈中弹出异常对象引用,如果该引用是null,则抛出NullPointerException异常,否则看是否可以找到并跳转到异常处理代码。

 

findAndGotoExceptionHandler()从当前帧开始,遍历Java虚拟机栈,查找方法的异常处理表。假设遍历到帧F,如果在F对应的方法中找不到异常处理项,则把F弹出,继续遍历。反之如果找到了异常处理项,在跳转到异常处理代码之前,要先把F的操作数栈清空,然后把异常对象引用推入栈顶。

 

 

如果遍历完Java虚拟机栈还是找不到异常处理代码,则handleUncaughtException()函数打印出Java虚拟机栈信息,handleUncaughtException()函数把Java虚拟机栈清空,然后打印出异常信息。由于Java虚拟机栈已经空了,所以解释器也就终止执行了。上面的代码使用Go语言的reflect包打印Java虚拟机栈信息。

 

Go语言的reflect包:

官网;reflect package – reflect – Go Packages

在Go语言中,reflect包中的ValueOf()函数是用于接收任何类型的变量并返回一个reflect.Value类型的函数。Value类型是reflect包中的一个类型,代表一个任意类型的变量。

 

reflect.Index() 函数用于获取切片、数组、指向该类型的指针或 interface 的子元素。它返回一个 reflect.Value 类型的值,表示访问元素后的结果。

 

reflect.Interface()函数接收一个Value类型的参数v,并返回一个interface{}类型的值。该函数返回的interface{}类型值是原始值的一份拷贝,只包含原始值的值部分,不包含类型信息部分。我们需要进行类型断言(type assertion)才能将其转换为具体的类型值。

 

如何使用reflect.Interface()函数来转换任意类型的变量,并使用类型断言将其转换为具体的类型值。这个过程可以归纳为以下几个步骤:

  1. 使用reflect.ValueOf()函数将任意类型的变量转换为reflect.Value类型的变量;
  2. reflect.Value类型的变量调用reflect.Interface()函数,将其转换为一个interface{}类型的值;
  3. 使用类型断言将interface{}类型的值转换为具体的类型值。

注意,reflect.Interface()函数只能用于导出的变量和方法。

还需要修改\instructions\factory.go文件,在NewInstruction()函数中增加athrow指令的case语句。

 

5.Java虚拟机栈信息

整体代码

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

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
}

 

在/rtda/thread中添加上述调用:

func (self *Thread) GetFrames() []*Frame {
    return self.stack.getFrames()
}

 

修改/rtda/heap/class.go文件,给Class结构体添加sourceFile字段,代码如下:

// name, superClassName and interfaceNames are all binary names(jvms8-4.2.1)

type Class struct {
    accessFlags       uint16
    name              string // thisClassName
    superClassName    string
    interfaceNames    []string
    constantPool      *ConstantPool
    fields            []*Field
    methods           []*Method
    sourceFile        string
    loader            *ClassLoader
    superClass        *Class
    interfaces        []*Class
    instanceSlotCount uint
    staticSlotCount   uint
    staticVars        Slots
    initStarted       bool
    jClass            *Object
}

 

也在/rtda/heap/class.go文件中添加SourceFile字段的getter方法。

func (self *Class) SourceFile() string {
    return self.sourceFile
}

 

修改newClass()函数,从class文件中读取源文件名:

func newClass(cf *classfile.ClassFile) *Class {
    class := &Class{}
    class.accessFlags = cf.AccessFlags()
    class.name = cf.ClassName()
    class.superClassName = cf.SuperClassName()
    class.interfaceNames = cf.InterfaceNames()
    class.constantPool = newConstantPool(class, cf.ConstantPool())
    class.fields = newFields(class, cf.Fields())
    class.methods = newMethods(class, cf.Methods())
    class.sourceFile = getSourceFile(cf)
    return class
}

 

源文件名在ClassFile结构的属性表中,getSourceFile()函数提取这个信息,

func getSourceFile(cf *classfile.ClassFile) string {
    if sfAttr := cf.SourceFileAttribute(); sfAttr != nil {
        return sfAttr.FileName()
    }
    return "Unknown" // todo
}

 

修改/classfile/class_file.go文件,添加获取SourceFileAttribute的方法:

func (self *ClassFile) SourceFileAttribute() *SourceFileAttribute {
    for _, attrInfo := range self.attributes {
        switch attrInfo.(type) {
        case *SourceFileAttribute:
            return attrInfo.(*SourceFileAttribute)
        }
    }
    return nil
}

 

修改/rtda/heap/method.go文件:修改Method结构体。给Method结构体添加lineNumberTable字段,改动如下:

type Method struct {
    ClassMember
    maxStack        uint
    maxLocals       uint
    code            []byte
    exceptionTable  ExceptionTable
    lineNumberTable *classfile.LineNumberTableAttribute
    argSlotCount    uint
}

 

也在/rtda/heap/method.go文件,copyAttributes()方法,从class文件中提取行号表,代码如下:

func (self *Method) copyAttributes(cfMethod *classfile.MemberInfo) {
    if codeAttr := cfMethod.CodeAttribute(); codeAttr != nil {
        self.maxStack = codeAttr.MaxStack()
        self.maxLocals = codeAttr.MaxLocals()
        self.code = codeAttr.Code()
        self.lineNumberTable = codeAttr.LineNumberTableAttribute()
        self.exceptionTable = newExceptionTable(codeAttr.ExceptionTable(),
        self.class.constantPool)
    }
}

 

也在/rtda/heap/method.go文件,添加的GetLineNumber()方法,和源文件名一样,并不是每个方法都有行号表。如果方法没有行号表,自然也就查不到pc对应的行号,这种情况下返回–1。本地方法没有字节码,所以返回–2。剩下的情况调用LineNumberTableAttribute结构体的GetLineNumber()方法查找行号,代码如下:

// 和源文件一样,并不是每个方法都有行号表。
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)
}

 

修改/classfile/attr_code.go文件,添加如下方法:

func (self *CodeAttribute) LineNumberTableAttribute() *LineNumberTableAttribute {
    for _, attrInfo := range self.attributes {
        switch attrInfo.(type) {
        case *LineNumberTableAttribute:
            return attrInfo.(*LineNumberTableAttribute)
        }
    }
    return nil
}

 

回到\native\java\lang\Throwable.go文件,在其中定义StackTraceElement结构体,代码如下:

package lang

import (
    "fmt"
    "jvmgo/ch10/native"
    "jvmgo/ch10/rtda"
    "jvmgo/ch10/rtda/heap"
)

const jlThrowable = "java/lang/Throwable"

// 该结构体用于记录Java虚拟机栈信息
type StackTraceElement struct {
    // 类所在的文件名
    fileName string
    // 声明方法的类名
    className string
    // 方法名
    methodName string
    // 帧正在执行哪行代码
    lineNumber int
}

func (self *StackTraceElement) String() string {
    return fmt.Sprintf("%s.%s(%s:%d)",
        self.className, self.methodName, self.fileName, self.lineNumber)
}

func init() {
    native.Register(jlThrowable, "fillInStackTrace", "(I)Ljava/lang/Throwable;", fillInStackTrace)
}

// private native Throwable fillInStackTrace(int dummy);
// (I)Ljava/lang/Throwable;
func fillInStackTrace(frame *rtda.Frame) {
    // 获取局部变量表第0位,this引用
    this := frame.LocalVars().GetThis()
    // 将this引用入栈
    frame.OperandStack().PushRef(this)
    // 创建完整的堆栈打印信息
    stes := createStackTraceElements(this, frame.Thread())
    // 将完整的堆栈信息赋值到异常类实例(this)的extra字段中,athrow指令中打印了extra信息
    this.SetExtra(stes)
}

func createStackTraceElements(tObj *heap.Object, thread *rtda.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 *rtda.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),
    }
}

 

代码解释

StackTraceElement结构体用来记录Java虚拟机栈帧信息:lineNumber字段给出帧正在执行哪行代码;methodName字段给出方法名;className字段给出声明方法的类名;fileName字段给出类所
在的文件名。

 

下面看fillInStackTrace(),重点在createStackTraceElements()函数里,代码如下:

 

createStackTraceElements()函数,由于栈顶两帧正在执行fillInStackTrace(int)和fillInStackTrace()方法,所以需要跳过这两帧。这两帧下面的几帧正在执行异常类的构造函数,所以也要跳过,具体要跳过多少帧数则要看异常类的继承层次。计算好需要跳过的帧之后,调用Thread结构体的GetFrames()方法拿到完整的Java虚拟机栈,然后reslice一下就是真正需要的帧。

 

distanceToObject()函数计算所需跳过的帧数,具体要跳过多少帧数则要看异常类的继承层次。代码如下:

 

createStackTraceElement()函数根据帧创建StackTraceElement实例,代码如下:

 

6.测试代码

打开命令行窗口,执行下面的命令编译本章代码。

go install jvmgo\ch10

测试下面的Java程序。

package jvmgo.book.ch09;

public class ParseIntTest {
    public static void main(String[] args) {
        foo(args);
    }

    private static void foo(String[] args) {
        try {
            bar(args);
        } catch (NumberFormatException e) {
            System.out.println(e.getMessage());
        }
    }

    private static void bar(String[] args) {
        if (args.length == 0) {
            throw new IndexOutOfBoundsException("no args!");
        }
        int x = Integer.parseInt(args[0]);
        System.out.println(x);
    }
}

结果:

目前,“Hello,World!”是可以出现在了控制台上。但是,由于java.lang.System类还没有被正确初
始化,而是直接调用System.out.println()方法会导致NullPointerException异常抛出。为此修改了invokevirtual指令,对println()方法做了特殊处理。还是使用的的hack技术。

而目前:class文件验证、内存管理和垃圾回收、类加载器的委派模型、多线程、JIT,等等都没有实现。以后有机会,再来尝试一下。

同时感谢原作者张秀宏大佬的自己动手写Java虚拟机学习,书中的知识对本人的提升很有帮助。

兜兜转转,至少我也实现了一个java虚拟机。

 

7.参考

尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ

周志明:深入理解java虚拟机

张秀宏:自己动手写Java虚拟机 (Java核心技术系列)

GO语言官网:Standard library – Go Packages

Java虚拟机规范:Chapter 4. The class File Format (oracle.com)

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇