代码、内容参考来自于张秀宏大佬的自己动手写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程序对类的使用分为两种:主动使用和被动使用。
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核心技术系列)


