手写JVM(二十一)-改进解析器和类初始化

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

下面修改之前的解释器

1.整体代码

修改/rtdata/jvm_stack.go文件,添加isEmpty()方法

func (self *Stack) isEmpty() bool {
    return self._top == nil
}

修改/rtdata/thread.go文件,添加IsStackEmpty()方法:

func (self *Thread) IsStackEmpty() bool {
    return self.stack.isEmpty()
}

 

让解释器支持方法调用。打开\interpreter.go文件,修改interpret()方法,代码如下:

package main

import (
    "fmt"
    "jvmgo/ch07/instructions"
    "jvmgo/ch07/instructions/base"
    "jvmgo/ch07/rtda"
    "jvmgo/ch07/rtda/heap"
)

func interpret(method *heap.Method, logInst bool) {
    thread := rtda.NewThread()
    frame := thread.NewFrame(method)
    thread.PushFrame(frame)

    defer catchErr(thread)
    loop(thread, logInst)
}

func catchErr(thread *rtda.Thread) {
    if r := recover(); r != nil {
        logFrames(thread)
        panic(r)
    }
}

func loop(thread *rtda.Thread, logInst bool) {
    // 实例化BytecodeReader
    reader := &base.BytecodeReader{}
    // 遍历Java虚拟机栈,直到栈顶指针为空
    for {
        // 获取Java虚拟机栈当前帧(栈顶指针)
        frame := thread.CurrentFrame()
        // 获取下个pc寄存器地址(也就是下一条将要执行的指令所在的位置)
        pc := frame.NextPC()
        // 设置线程pc寄存器为当前帧的下一个指令地址
        thread.SetPC(pc)

        // decode
        //重置reader
        reader.Reset(frame.Method().Code(), pc)
        // 获取指令操作码
        opcode := reader.ReadUint8()
        // 根据操作码常见指令,参照factory.go文件
        inst := instructions.NewInstruction(opcode)
        // 读取操作数
        inst.FetchOperands(reader)
        // 设置下一个指令起始地址(下一条指令操作码在字节码中的位置)
        frame.SetNextPC(reader.PC())
        // 是否将执行信息打印到控制台
        if logInst {
            logInstruction(frame, inst)
        }

        // 执行指令
        inst.Execute(frame)
        // 若栈顶指针为空,表示线程执行结束
        if thread.IsStackEmpty() {
            break
        }
    }
}

func logInstruction(frame *rtda.Frame, inst base.Instruction) {
    method := frame.Method()
    className := method.Class().Name()
    methodName := method.Name()
    pc := frame.Thread().PC()
    fmt.Printf("%v.%v() #%2d %T %v\n", className, methodName, pc, inst, inst)
}

func logFrames(thread *rtda.Thread) {
    for !thread.IsStackEmpty() {
        frame := thread.PopFrame()
        method := frame.Method()
        className := method.Class().Name()
        fmt.Printf(">> pc:%4d %v.%v%v \n",
        frame.NextPC(), className, method.Name(), method.Descriptor())
    }
}

 

2.代码解释

logInst参数控制是否把指令执行信息打印到控制台。代码如下所示:

 

在loop()函数中,在每次循环开始,先拿到当前帧,然后根据pc从当前方法中解码出一条指令。指令执行完毕之后,判断Java虚拟机栈中是否还有帧。如果没有则退出循环;否则继续。

 

如果解释器在执行期间出现了问题,catchErr()函数会打印出错信息,代码如下:

logFrames()函数打印Java虚拟机栈信息,logInstruction()函数在方法执行过程中打印指令信息,

 

解释器改造完毕,下面测试方法调用。

 

3.测试方法调用

先改造命令行工具,给它增加两个选项。java命令提供了-verbose:class(简写为-verbose)选项,可以控制是否把类加载信息输出到控制台。另外参照这个选项增加一个-verbose:inst选项,用来控制是否把指令执行信息输出到控制台。
打开cmd.go文件,修改Cmd结构体如下:

// java [-options] class [args...]
type Cmd struct {
    helpFlag         bool
    versionFlag      bool
    verboseClassFlag bool
    verboseInstFlag  bool
    cpOption         string
    XjreOption       string
    class            string
    args             []string
}

 

parseCmd()函数也需要修改。

func parseCmd() *Cmd {
    cmd := &Cmd{}

    flag.Usage = printUsage
    flag.BoolVar(&cmd.helpFlag, "help", false, "print help message")
    flag.BoolVar(&cmd.helpFlag, "?", false, "print help message")
    flag.BoolVar(&cmd.versionFlag, "version", false, "print version and exit")
    flag.BoolVar(&cmd.verboseClassFlag, "verbose", false, "enable verbose output")
    flag.BoolVar(&cmd.verboseClassFlag, "verbose:class", false, "enable verbose output")
    flag.BoolVar(&cmd.verboseInstFlag, "verbose:inst", false, "enable verbose output")
    flag.StringVar(&cmd.cpOption, "classpath", "", "classpath")
    flag.StringVar(&cmd.cpOption, "cp", "", "classpath")
    flag.StringVar(&cmd.XjreOption, "Xjre", "", "path to jre")
    flag.Parse()

    args := flag.Args()
    if len(args) > 0 {
        cmd.class = args[0]
        cmd.args = args[1:]
    }

    return cmd
}

 

下面修改main.go文件,其他地方不变,只需要修改startJVM()函数,代码如下:

func startJVM(cmd *Cmd) {
    cp := classpath.Parse(cmd.XjreOption, cmd.cpOption)
    classLoader := heap.NewClassLoader(cp, cmd.verboseClassFlag)

    className := strings.Replace(cmd.class, ".", "/", -1)
    mainClass := classLoader.LoadClass(className)
    mainMethod := mainClass.GetMainMethod()
    if mainMethod != nil {
        interpret(mainMethod, cmd.verboseInstFlag)
    } else {
        fmt.Printf("Main method not found in class %s\n", cmd.class)
    }
}

然后修改\rtda\heap\class_loader.go文件,给ClassLoader结构体添加verboseFlag字段,代码如下:

type ClassLoader struct {
    cp          *classpath.Classpath
    verboseFlag bool
    classMap    map[string]*Class // loaded classes
}

NewClassLoader()函数要相应修改,改动如下:

func NewClassLoader(cp *classpath.Classpath, verboseFlag bool) *ClassLoader {
    return &ClassLoader{
        cp:          cp,
        verboseFlag: verboseFlag,
        classMap:    make(map[string]*Class),
    }
}

loadNonArrayClass()函数也要修改,改动如下:

func (self *ClassLoader) loadNonArrayClass(name string) *Class {
    data, entry := self.readClass(name)
    class := self.defineClass(data)
    link(class)

    if self.verboseFlag {
        fmt.Printf("[Loaded %s from %s]\n", name, entry)
    }

    return class
}

 

使用java计算斐波那契数列的代码

package jvmgo.book.ch06;

public class FibonacciTest {
    public static void main(String[] args) {
        long x = fibonacci(30);
        System.out.println(x);
    }

    private static long fibonacci(long n) {
        if (n <= 1) {
            return n;
        }
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}

 

一切都准备就绪,打开命令行窗口,执行下面的命令编译本章代码:

go install jvmgo\ch07

使用exe运行上述java代码的class文件

mian方法pc默认1

ReadUint8的pc加1

 

 

4.类初始化

Java程序对类的使用分为两种:主动使用和被动使用。

主动使用意味着会调用类的<clinit>(),即执行了类的初始化阶段

Class只有在必须要首次使用的时候才会被装载,Java虚拟机不会无条件地装载Class类型,Java虚拟机规定,一个类或接口在初次使用前,必须要进行初始化,这里指的“使用”,是指主动使用,主动使用只有下列几种情况: (即:如果出现如下的情况,则会对类进行初始化操作。而初始化操作之前的加载、验证、准备已经完成。):

  • 1.当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
  • 2.当调用类的静态方法时,即当使用了字节码invokestatic指令。
  • 3.当使用类、接口的静态字段时(final修饰特殊考虑,比如,使用getstatic或者putstatic指令。 (对应访问变量、赋值变量操作)
  • 4.当使用java.lang.reflect包中的方法反射类的方法时。比如: Class.forName(“com.yutian.java.Test”)
  • 5. 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 6.如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。(不管调没调用该方法)
  • 7.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。(执行main()方法,会初始化当前主类)
  • 8.当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。(涉及解析REF_getStatic、 REF_putStatic、REF_invokeStatic方法句柄对应的类)

针对5,补充说明:当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。

  • 在初始化一个类时,并不会先初始化它所实现的接口
  • 在初始化一个接口时,并不会先初始化它的父接口

因此,一个父接口井不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化。

针对7,说明:JVM启动的时候通过引导类加载器加载一个初始类。这个类在调用public static void main(String[])方法之前被链接和初始化。这个方法的执行将依次导致所需的类的加载,链接和初始化。

针对3.当使用类、接口的静态字段时,final修饰不一样,除非final修饰又调用方法,才初始化

整体代码

为了判断类是否已经初始化,需要给Class结构体添加一个字段,类的初始化其实分为几个阶段,但由于我们的类加载器还不够完善,所以先使用一个简单的布尔状态就足够了。initStarted字段表示类的<clinit>方法是否已经开始执行。

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

接下来给Class结构体添加两个方法,InitStarted()是Getter方法,返回initStarted字段值。StartInit()方法把initStarted字段设置成true。

func (self *Class) InitStarted() bool {
    return self.initStarted
}

func (self *Class) StartInit() {
    self.initStarted = true
}

 

在/instructions/references/new.go文件下面修改new指令,代码如下:

func (self *NEW) Execute(frame *rtda.Frame) {
    cp := frame.Method().Class().ConstantPool()
    classRef := cp.GetConstant(self.Index).(*heap.ClassRef)
    class := classRef.ResolvedClass()
    if !class.InitStarted() {
        frame.RevertNextPC()
        base.InitClass(frame.Thread(), class)
        return
    }

    if class.IsInterface() || class.IsAbstract() {
        panic("java.lang.InstantiationError")
    }

    ref := class.NewObject()
    frame.OperandStack().PushRef(ref)
}

先判断类的初始化是否已经开始,如果还没有,则需要调用类的初始化方法,并终止指令执行。但是由于此时指令已经执行到了一半,也就是说当前帧的nextPC字段已经指向下一条指令,所以需要修改nextPC,让它重新指向当前指令。

该方法在Frame结构体中,修改/rtda/frame.go文件,添加RevertNextPC()方法,代码如下:

func (self *Frame) RevertNextPC() {
 //重置nextPC,因为Thread的pc寄存器字段始终指向当前指令地址
    self.nextPC = self.thread.pc
}

 

nextPC调整好之后,下一步查找并调用类的初始化方法。这个逻辑是通用的,新建\instructions\base\class_init_logic.go文件,代码如下:

package base

import (
    "jvmgo/ch07/rtda"
    "jvmgo/ch07/rtda/heap"
)

// jvms 5.5
func InitClass(thread *rtda.Thread, class *heap.Class) {
    //InitClass()函数先调用StartInit()方法把类的initStarted状态设置成true以免进入死循环
    class.StartInit()
    //调用scheduleClinit()函数准备执行类的初始化方法
    scheduleClinit(thread, class)
    initSuperClass(thread, class)
}

func scheduleClinit(thread *rtda.Thread, class *heap.Class) {
    clinit := class.GetClinitMethod()
    if clinit != nil {
        // exec <clinit>
        newFrame := thread.NewFrame(clinit)
        thread.PushFrame(newFrame)
    }
}

// 可能要先执行超类的初始化方法
func initSuperClass(thread *rtda.Thread, class *heap.Class) {
    if !class.IsInterface() {
        superClass := class.SuperClass()
        //如果超类的初始化还没有开始,就递归调用InitClass()函数执行超类的初始化方法
        if superClass != nil && !superClass.InitStarted() {
            InitClass(thread, superClass)
        }
    }
}

 

代码解释

InitClass()函数先调用StartInit()方法把类的initStarted状态设置成true以免进入死循环,然后调用scheduleClinit()函数准备执行类的初始化方法,代码如下:

类初始化方法没有参数,所以不需要传递参数。在/rtda/heap/class.go加入GetClinitMethod()方法如下:

func (self *Class) GetClinitMethod() *Method {
    return self.getStaticMethod("<clinit>", "()V")
}

 

如果超类的初始化还没有开始,就递归调用InitClass()函数执行超类的初始化方法,这样可以保证超类的初始化方法对应的帧在子类上面,使超类初始化方法先于子类执行。

 

同时putstatic、getstatic和invokestatic指令的Execute()中也需要添加类初始化方法逻辑

putstatic指令,代码如下:

加在这个位置:

if !class.InitStarted() {
    frame.RevertNextPC()
    base.InitClass(frame.Thread(), class)
    return
}

 

getstatic指令:

加在这个位置:

if !class.InitStarted() {
    frame.RevertNextPC()
    base.InitClass(frame.Thread(), class)
    return
}

 

invokestatic指令也需要修改,改动如下:

加在这个位置:

class := resolvedMethod.Class()
if !class.InitStarted() {
    frame.RevertNextPC()
    base.InitClass(frame.Thread(), class)
    return
}

 

由于目前还不支持本地方法调用,而Java类库中的很多类都要注册本地方法,比如Object类就有一个registerNatives()本地方法,用于注册其他方法,由于Object类是其他所有类的超类,所以这会导致Java虚拟机崩溃。

解决办法是修改InvokeMethod()函数(代码在ch07\instructions\base\method_invoke_logic.go文件中),让它跳过所有registerNatives()方法,改动如下:

加在这个位置:

// hack!
if method.IsNative() {
    if method.Name() == "registerNatives" {
        thread.PopFrame()
    } else {
        panic(fmt.Sprintf("native method: %v.%v%v\n",
        method.Class().Name(), method.Name(), method.Descriptor()))
    }
}

 

修改/rtda/heap/method.go文件,添加IsNative()方法:

func (self *Method) IsNative() bool {
    return 0 != self.accessFlags&ACC_NATIVE
}

如果遇到其他本地方法,直接调用panic()函数终止程序执行即可。

 

5.参考

尚硅谷宋红康: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
小恐龙
花!
上一篇
下一篇