bboyjing's blog

自己动手写JVM二十八【数组和字符串(三)】

字符串

在class文件中,字符串是以MUTF8格式保存的,这一点之前了解过了。在Java虚拟机运行期间,字符串以java.lang.String对象的形式存在,而在String对象内部,字符串又是以UTF16格式保存。字符串相关功能大部分都是由String(和StringBuilder类等)类提供的,本节我们只学习一些辅助功能即可。
String类有两个实例变量,其中一个是value,类型是字符数组,用于存放UTF16编码后的字符序列。另一个是hash,缓存字符串的哈希码。有兴趣可以再看下Java中String类的实现。

字符串池

在Java中,String类提供了intern()实例方法,可以把自己放入字符串池,这里我们也来实现这样一个字符串池的功能。实现代码位于/rtdata/heap/string_pool.go文件中。在此之前,需要添加反射功能:

  1. 修改/rtdata/heap/class.go文件,添加通过名称和描述符获取成员变量的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 通过名称和描述符获取成员变量
    func (self *Class) getField(name, descriptor string, isStatic bool) *Field {
    for c := self; c != nil; c = c.superClass {
    for _, field := range c.fields {
    if field.IsStatic() == isStatic &&
    field.name == name &&
    field.descriptor == descriptor {
    return field
    }
    }
    }
    return nil
    }
  2. 修改/rtdata/heap/object.go文件,添加反射支持:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 通过反射获取属性的值
    func (self *Object) GetRefVar(name, descriptor string) *Object {
    field := self.class.getField(name, descriptor, false)
    slots := self.data.(Slots)
    return slots.GetRef(field.slotId)
    }
    // 通过反射设置属性的值
    func (self *Object) SetRefVar(name, descriptor string, ref *Object) {
    field := self.class.getField(name, descriptor, false)
    slots := self.data.(Slots)
    slots.SetRef(field.slotId, ref)
    }

下面就来实现字符串池功能:

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
// 用map来表示字符串池,key是Go字符串,value是Java字符串
var internedStrings = map[string]*Object{}
// 根据Go自复查un返回相应的Java字符串实例
func JString(loader *ClassLoader, goStr string) *Object {
// 如果在字符串池中存在,直接返回
if internedStr, ok := internedStrings[goStr]; ok {
return internedStr
}
// 将utf8转成utf16
chars := stringToUtf16(goStr)
// 字符串实例引用
jChars := &Object{loader.LoadClass("[C"), chars}
// 加载String类,并且创建实例
jStr := loader.LoadClass("java/lang/String").NewObject()
// 通过反射,给实例的char[]类型的value变量设值
jStr.SetRefVar("value", "[C", jChars)
// 将字符串添加到常量池
internedStrings[goStr] = jStr
return jStr
}
// utf8 -> utf16
func stringToUtf16(s string) []uint16 {
runes := []rune(s) // utf32
return utf16.Encode(runes) // func Encode(s []rune) []uint16
}

完善ldc指令

修改/instructions/constants/ldc.go文件的_ldc()方法,主要的修改如下:

1
2
3
4
5
6
7
8
9
10
11
func _ldc(frame *rtdata.Frame, index uint) {
...
// 获取当前类
class := frame.Method().Class()
...
case string:
// 从字符串常量池中获取Java字符串
internedStr := heap.JString(class.Loader(), c.(string))
stack.PushRef(internedStr)
...
}

完善类加载器

修改/rtdata/heap/class_loader.go文件的initStaticFinalVar()方法,只要添加String的case就可以了:

1
2
3
4
5
// String类型
case "Ljava/lang/String;":
goStr := cp.GetConstant(cpIndex).(string)
jStr := JString(class.Loader(), goStr)
vars.SetRef(slotId, jStr)

测试字符串

  1. 修改main.go文件的startJVM()函数,仅需要修改一行:

    1
    instructions.Interpret(mainMethod, cmd.VerboseInstFlag, cmd.Args)
  2. 修改interpreter.go文件的Interpret()方法,添加args参数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    func Interpret(method *heap.Method, logInst bool, args []string) {
    ...
    // 将参数转成Object引用
    jArgs := createArgsArray(method.Class().Loader(), args)
    // 将引用存储到局部变量表第0位
    frame.LocalVars().SetRef(0, jArgs)
    ...
    }
    // 生成java字符串数组
    func createArgsArray(loader *heap.ClassLoader, args []string) *heap.Object {
    // 加载java.lang.String类
    stringClass := loader.LoadClass("java/lang/String")
    // 创建数组
    argsArr := stringClass.ArrayClass().NewArray(uint(len(args)))
    jArgs := argsArr.Refs()
    for i, arg := range args {
    // 获取Java String
    jArgs[i] = heap.JString(loader, arg)
    }
    return argsArr
    }
  3. 修改/instructions/references/invokevirtual.go文件的_print()函数,支持打印字符串:

    1
    2
    3
    4
    case "(Ljava/lang/String;)V":
    jStr := stack.PopRef()
    goStr := heap.GoString(jStr)
    fmt.Println(goStr)
  4. 修改/rtdata/heap/string_pool.go文件,添加Java字符串转Go字符串的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // java.lang.String -> go string
    func GoString(jStr *Object) string {
    charArr := jStr.GetRefVar("value", "[C")
    return utf16ToString(charArr.Chars())
    }
    // utf16 -> utf8
    func utf16ToString(s []uint16) string {
    runes := utf16.Decode(s) // func Decode(s []uint16) []rune
    return string(runes)
    }
  5. 测试HelloWorld,新建HelloWorld.java,代码参照项目源码。

    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.HelloWorld
    // 输出如下
    Hello, world!
  6. 再测试个带参数的例子,新建PrintArgs.java,代码也比较简单,参照项目源码。

    1
    2
    3
    4
    5
    6
    cd /home/zhangjing/IdeaProjects/jvmgo/go
    go install cn.didadu/jvmgo
    ./bin/jvmgo -classpath /home/zhangjing/IdeaProjects/jvmgo/java/target/classes cn.didadu.PrintArgs Hello World!
    // 输出如下
    Hello
    World!

数组和字符串的学习就到此结束了。