bboyjing's blog

自己动手写JVM二十五【方法调用和返回(三)】

本节继续实现方法调用和返回。

测试

首先要改造命令行工具,增加两个选项。java命令提供-verbose:class选项,可以控制是否把类加载信息输出到控制台。另外参照这个选项增加一个-verbose:inst选项,用来控制是否把指令执行信息输出到控制台,下面是测试步骤:

  1. 修改/cmd/cmd.go文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    type Cmd struct {
    ...
    VerboseClassFlag bool
    VersionFlag 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")
    ...
    }
  2. 修改/rtdata/heap/class_loader.go文件,添加verboseFlag:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    type 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)
    }
    ...
    }
  3. 修改main.go文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    func startJVM(cmd *cmd.Cmd) {
    // 获取Classpath
    cp := 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)
    }
    }
  4. 新增一个测试类到java项目中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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;
    } else {
    return fibonacci(n - 1) + fibonacci(n - 2);
    }
    }
    }
  5. 测试

    1
    2
    3
    4
    5
    cd /home/zhangjing/IdeaProjects/jvmgo/go
    go 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>方法。
jvmgo_44

要实现类初始化功能需要修改挺多地方,下面我们一个个来看:

  1. 为了判断类是否已经初始化,需要给Class结构体添加新字段,修改/rtdata/heap/class.go文件:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    type Class struct {
    ...
    // 表示类的<clinit>方法是否已经开始执行
    initStarted bool
    }
    func (self *Class) InitStarted() bool {
    return self.initStarted
    }
    func (self *Class) StartInit() {
    self.initStarted = true
    }
  1. 修改new指令,在Execute()方法中添加如下代码,/instructions/references/new.go文件。判断逻辑中就三行代码,有两行还没实现,先解释下。主要功能就是调用类的初始化方法,并终止当前指令的执行,但是由于此时指令已经执行一部分,也就是说当前帧的nextPC字段已经指向了下一条指令,所以需要修改nextPC,让它重新指向当前指令。

    1
    2
    3
    4
    5
    6
    // 判断类初始化是否已经开始
    if !class.InitStarted() {
    frame.RevertNextPC()
    base.InitClass(frame.Thread(), class)
    return
    }
  2. 实现RevertNextPC()方法,该方法在Frame结构体中,修改/rtdata/frame.go文件:

    1
    2
    3
    4
    5
    // 重置nextPC
    func (self *Frame) RevertNextPC() {
    // 因为Thread的pc寄存器字段始终指向当前指令地址
    self.nextPC = self.thread.pc
    }
  3. 实现类的初始化方法,这个逻辑是通用的,新建/instructions/base/class_init_logic.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
    26
    27
    28
    29
    30
    31
    32
    33
    // 类初始化方法
    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)
    }
    }
    }
  4. 实现GetClinitMethod()方法,修改/rtdata/heap/class.go文件:

    1
    2
    3
    func (self *Class) GetClinitMethod() *Method {
    return self.getStaticMethod("<clinit>", "()V")
    }
  5. putstatic、getstatic和invokestatic指令的Execute()中也需要添加类初始化方法逻辑,代码和new指令一样,这里就不贴出来了,参照项目源码。

  6. 最后还需要增加一个hack。由于目前还不支持本地方法调用,而Java类库中的很多类都要注册本地方法,比如Object类就有一个registerNatives()本地方法,用于注册其他方法。由于Object类是其他所有类的超类,所以这会导致Java虚拟机崩溃。解决办法是修改InvokeMethod()函数,跳过所有registerNatives()方法。修改/instructions/base/method_invoke_logic.go文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    func 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()))
    }
    }
    }
  7. 修改/rtdata/heap/method.go文件,添加IsNative()方法:

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

方法调用和返回的学习就到此结束。