本节继续实现方法调用和返回。
测试
首先要改造命令行工具,增加两个选项。java命令提供-verbose:class选项,可以控制是否把类加载信息输出到控制台。另外参照这个选项增加一个-verbose:inst选项,用来控制是否把指令执行信息输出到控制台,下面是测试步骤:
修改/cmd/cmd.go文件:
12345678910111213type Cmd struct {...VerboseClassFlag boolVersionFlag bool...}func ParseCmd() *Cmd {...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")...}修改/rtdata/heap/class_loader.go文件,添加verboseFlag:
123456789101112131415type ClassLoader struct {...// 是否把类加载信息输出到控制台verboseFlag bool}func NewClassLoader(cp *classpath.Classpath, verboseFlag bool) *ClassLoader {...}func (self *ClassLoader) loadNonArrayClass(name string) *Class {...if self.verboseFlag {fmt.Printf("[Loaded %s from %s]\n", name, entry)}...}修改main.go文件:
1234567891011121314151617func startJVM(cmd *cmd.Cmd) {// 获取Classpathcp := classpath.Parse(cmd.XjreOption, cmd.CpOption)classLoader := heap.NewClassLoader(cp, cmd.VerboseClassFlag)// class权限定名,将.替换成/(java.lang.String -> java/lang/String)className := strings.Replace(cmd.Class, ".", "/", -1)// 加载主类mainClass := classLoader.LoadClass(className)// 获取主类的main()方法mainMethod := mainClass.GetMainMethod()if mainMethod != nil {instructions.Interpret(mainMethod, cmd.VerboseInstFlag)} else {fmt.Printf("Main method not found in class %s\n", cmd.Class)}}新增一个测试类到java项目中:
1234567891011121314public 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;} else {return fibonacci(n - 1) + fibonacci(n - 2);}}}测试
12345cd /home/zhangjing/IdeaProjects/jvmgo/gogo install cn.didadu/jvmgo./bin/jvmgo -classpath /home/zhangjing/IdeaProjects/jvmgo/java/target/classes cn.didadu.FibonacciTest//输出如下832040
类初始化
之前实现了一个简化版的类加载器,可以把类加载到方法区中。但是因为当时还没有实现方法调用,所以没有办法初始化类。现在可以把这个逻辑补上了。我们已经知道了类初始化就是执行类的初始化方法(<clinit>)。类的初始化在下列情况下触发:
- 执行new指令创建类实例,但是类还没有被初始化。
- 执行putstatic、getstatic指令存取类的静态变量,但声明该字段的类还没有被初始化。
- 执行invokestatic调用类的静态方法,但声明该方法的类还没有被初始化。
- 当初始化一个类时,如果类的超类还没有被初始化,要先初始化类的超类。
- 执行某些反射操作时
这里穿插一下,在之前写的ClassFile.class中怎么都没有看到<clinit>方法,只看到<init>方法。稍微查了下资料,<clinit>方法是编译器自动生成的,用来执行静态变量初始化语句和静态块的。但是ClassFile有静态变量呀,再仔细一看,ClassFile的静态变量都是final的,编译的时候就可以确定值了,所以是在链接阶段(ClassLoader.link())就初始化好了。只要稍作修改,随便把其中一个变量的final拿掉public static double E = 2.71828;
,在字节码中就能够看到<clinit>方法了。另外<init>方法其实就是默认无参构造函数,如果自己没有声明的话,由编译器自动生成。如果有多个构造函数的话,字节码中就会有多个<init>方法。
要实现类初始化功能需要修改挺多地方,下面我们一个个来看:
- 为了判断类是否已经初始化,需要给Class结构体添加新字段,修改/rtdata/heap/class.go文件: 12345678910111213type Class struct {...// 表示类的<clinit>方法是否已经开始执行initStarted bool}func (self *Class) InitStarted() bool {return self.initStarted}func (self *Class) StartInit() {self.initStarted = true}
修改new指令,在Execute()方法中添加如下代码,/instructions/references/new.go文件。判断逻辑中就三行代码,有两行还没实现,先解释下。主要功能就是调用类的初始化方法,并终止当前指令的执行,但是由于此时指令已经执行一部分,也就是说当前帧的nextPC字段已经指向了下一条指令,所以需要修改nextPC,让它重新指向当前指令。
123456// 判断类初始化是否已经开始if !class.InitStarted() {frame.RevertNextPC()base.InitClass(frame.Thread(), class)return}实现RevertNextPC()方法,该方法在Frame结构体中,修改/rtdata/frame.go文件:
12345// 重置nextPCfunc (self *Frame) RevertNextPC() {// 因为Thread的pc寄存器字段始终指向当前指令地址self.nextPC = self.thread.pc}实现类的初始化方法,这个逻辑是通用的,新建/instructions/base/class_init_logic.go文件:
123456789101112131415161718192021222324252627282930313233// 类初始化方法func InitClass(thread *rtdata.Thread, class *heap.Class) {// 设置类初始化开始标志class.StartInit()scheduleClinit(thread, class)initSuperClass(thread, class)}func scheduleClinit(thread *rtdata.Thread, class *heap.Class) {// 获取<clinit>方法clinit := class.GetClinitMethod()if clinit != nil {// 创建新的帧newFrame := thread.NewFrame(clinit)// 将新建的帧推入Java虚拟机栈thread.PushFrame(newFrame)}}// 初始化超类func initSuperClass(thread *rtdata.Thread, class *heap.Class) {if !class.IsInterface() {superClass := class.SuperClass()if superClass != nil && !superClass.InitStarted() {/*递归调用InitClass()方法这样就可以保证超类的初始化方法对应的帧在子类上面,使超类初始化方法先于子类*/InitClass(thread, superClass)}}}实现GetClinitMethod()方法,修改/rtdata/heap/class.go文件:
123func (self *Class) GetClinitMethod() *Method {return self.getStaticMethod("<clinit>", "()V")}putstatic、getstatic和invokestatic指令的Execute()中也需要添加类初始化方法逻辑,代码和new指令一样,这里就不贴出来了,参照项目源码。
最后还需要增加一个hack。由于目前还不支持本地方法调用,而Java类库中的很多类都要注册本地方法,比如Object类就有一个registerNatives()本地方法,用于注册其他方法。由于Object类是其他所有类的超类,所以这会导致Java虚拟机崩溃。解决办法是修改InvokeMethod()函数,跳过所有registerNatives()方法。修改/instructions/base/method_invoke_logic.go文件:
123456789101112func InvokeMethod(invokerFrame *rtdata.Frame, method *heap.Method) {...// 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()))}}}修改/rtdata/heap/method.go文件,添加IsNative()方法:
123func (self *Method) IsNative() bool {return 0 != self.accessFlags&ACC_NATIVE}
方法调用和返回的学习就到此结束。