bboyjing's blog

自己动手写JVM二十二【类和对象(五)】

类和对象相关指令

本节继续来学习类和对象剩下的ldc指令,和测试。

ldc指令

ldc系列指令从运行时常量池中加载常量值,并把它推入操作数栈。ldc系列指令属于常量类指令,共3条。其中ldc和ldc_w指令用于加载int、float和字符串常量,java.lang.Class实例或者MethodType和MethodHandle实例。ldc2_w指令用于加载long和double常量。ldc和ldc_w指令的区别仅在于操作数的宽度。本节先处理int、float、long和double常量,部分其余的后面再学。
ldc系列指令位于/instructions/constants/ldc.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
34
35
// ldc指令结构体(单字节操作数)
type LDC struct{ base.Index8Instruction }
func (self *LDC) Execute(frame *rtdata.Frame) {
_ldc(frame, self.Index)
}
func _ldc(frame *rtdata.Frame, index uint) {
// 获取操作数栈
stack := frame.OperandStack()
// 获取运行时常量池
cp := frame.Method().Class().ConstantPool()
// 通过索引从常量池中获取常量值
c := cp.GetConstant(index)
// 根据常量值的类型将对应的值入栈
switch c.(type) {
case int32:
stack.PushInt(c.(int32))
case float32:
stack.PushFloat(c.(float32))
// case string:
// case *heap.ClassRef:
// case MethodType, MethodHandle
default:
panic("todo: ldc!")
}
}名,将.替换成/(java.lang.String -> java/lang/String)
className := strings.Replace(cmd.Class, ".", "/", -1)
// 加载主类
mainClass := classloader.LoadClass(className);
// 获取主类的main()方法
// 省略ldc_w、ldc2_w指令
...

测试

下面我们来测试下本章实现的类和对象相关代码,列出测试步骤:

  1. 修改/rtdata/heap/class.go文件,添加获取main方法的函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 获取main()方法
    func (self *Class) GetMainMethod() *Method {
    return self.getStaticMethod("main", "([Ljava/lang/String;)V")
    }
    // 通过方法名和描述符获取静态方法
    func (self *Class) getStaticMethod(name, descriptor string) *Method {
    // 遍历运行时常量池中的方法信息
    for _, method := range self.methods {
    if method.IsStatic() &&
    method.name == name &&
    method.descriptor == descriptor {
    return method
    }
    }
    return nil
    }
  2. 修改/instructions/interpreter.go文件的Interpret()函数,并且注释catchErr()函数中的部分代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 传入入口方法,初始化线程
    func Interpret(method *heap.Method) {
    // 创建一个Thread实例
    thread := rtdata.NewThread()
    // 创建栈帧
    frame := thread.NewFrame(method)
    // 栈帧推入虚拟栈
    thread.PushFrame(frame)
    defer catchErr(frame)
    loop(thread, method.Code())
    }
    func catchErr(frame *rtda.Frame) {
    if r := recover(); r != nil {
    //fmt.Printf("LocalVars:%v\n", frame.LocalVars())
    //fmt.Printf("OperandStack:%v\n", frame.OperandStack())
    //panic(r)
    }
    }
  3. 在测试之前还需要添加两个hack:

    1. 因为对象是需要初始化的,所以每个类至少有一个构造函数。即使用户自己不定义,编译器也会自动生成一个默认构造函数。在创建类实例时,编译器会在new指令的后面加入invokespecial指令来调用构造函数初始化对象。后面才会实现invokespecial指令,为了测试putfield和getfield等指令,这里先给一个空的实现。新建/instructions/references/invokespecial.go文件:

      1
      2
      3
      4
      5
      6
      type INVOKE_SPECIAL struct{ base.Index16Instruction }
      // hack!
      func (self *INVOKE_SPECIAL) Execute(frame *rtdata.Frame) {
      frame.OperandStack().PopRef()
      }
    2. 指令集和解释器章节通过打印局部变量表和操作数的方式观察计算结果,这样很不方便。这里用另外一个hack来解决这个问题,新建/instructions/references/invokevirtual.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
      // Invoke instance method; dispatch based on class
      type INVOKE_VIRTUAL struct{ base.Index16Instruction }
      // hack!
      func (self *INVOKE_VIRTUAL) Execute(frame *rtdata.Frame) {
      // 获取运行时常量池
      cp := frame.Method().Class().ConstantPool()
      // 获取方法符号引用
      methodRef := cp.GetConstant(self.Index).(*heap.MethodRef)
      // 当方法为println
      if methodRef.Name() == "println" {
      // 获取操作数栈
      stack := frame.OperandStack()
      // 通过描述符从操作数栈中弹出对应的值
      switch methodRef.Descriptor() {
      case "(Z)V":
      fmt.Printf("%v\n", stack.PopInt() != 0)
      case "(C)V":
      fmt.Printf("%c\n", stack.PopInt())
      case "(I)V", "(B)V", "(S)V":
      fmt.Printf("%v\n", stack.PopInt())
      case "(F)V":
      fmt.Printf("%v\n", stack.PopFloat())
      case "(J)V":
      fmt.Printf("%v\n", stack.PopLong())
      case "(D)V":
      fmt.Printf("%v\n", stack.PopDouble())
      default:
      panic("println: " + methodRef.Descriptor())
      }
      stack.PopRef()
      }
      }
  4. 修改/instructions/factory.go文件,添加本章实现的指令,只要把注释放开就好,代码就不贴出来了。

  5. 修改测试入口main.go文件的startJVM()函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    func startJVM(cmd *cmd.Cmd) { if mainClass != nil {
    // 获取Classpath
    cp := classpath.Parse(cmd.XjreOption, cmd.CpOption)
    // 创ClassLoader实例
    classloader := heap.NewClassLoader(cp)
    // class权限定名,将.替换成/(java.lang.String -> java/lang/String)
    className := strings.Replace(cmd.Class, ".", "/", -1)
    // 加载主类
    mainClass := classloader.LoadClass(className);
    // 获取主类的main()方法
    mainMethod := mainClass.GetMainMethod()
    if mainClass != nil {
    instructions.Interpret(mainMethod)
    } else {
    fmt.Printf("Main method not found in class %s\n", cmd.Class)
    }
    }
  6. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    cd /home/zhangjing/IdeaProjects/jvmgo/go
    go install cn.didadu/jvmgo
    #指定-classpath
    ./bin/jvmgo -classpath /home/zhangjing/IdeaProjects/jvmgo/java/target/classes cn.didadu.MyObject
    // 部分打印信息如下
    [Loaded java/lang/Object from /usr/lib/jvm/jdk1.8.0_91/jre/lib/rt.jar]
    [Loaded cn/didadu/MyObject from /home/zhangjing/IdeaProjects/jvmgo/java/target/classes]
    pc: 0 inst:*constants.LDC &{{2}}
    pc: 2 inst:*stores.ISTORE_1 &{{}}
    ...
    32768

    至此,本章的代码已实现完毕,对类的加载过程有了进一步的了解。