代码、内容参考来自于张秀宏大佬的自己动手写Java虚拟机 (Java核心技术系列)以及尚硅谷宋红康:JVM全套教程。
1.字符串概述
在class文件中,字符串是以MUTF8格式保存的,在Java虚拟机运行期间,字符串以java.lang.String(后面简称String)对象的形式存在,而在String对象内部,字符串又是以UTF16格式保存的。字符串相关功能大部分都是由String(和StringBuilder等)类提供的,本节只实现一些辅助功能即可。String类有两个实例变量。其中一个是value,类型是字符数组,用于存放UTF16编码后的字符序列。另一个是hash,缓存计字符串的哈希码,代码如下:

字符串对象是不可变(immutable)的,一旦构造好之后,就无法再改变其状态(这里指value字段)。String类有很多构造函数,其中一个是根据字符数组来创建String实例,代码如下:
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}本节将参考上面的构造函数,直接创建String实例。
2.字符串池
整体代码
修改/rtda/heap/class.go文件,添加Class结构体的getField()函数根据字段名和描述符查找字段,也就是反射功能。
// 通过名称和描述符获取成员变量
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
}
Object结构体的SetRefVar()方法直接给对象的引用类型实例变量赋值,代码如下(在object.go文件中):
// reflection
// 通过反射获取属性的值
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)
}
在\rtda\heap目录下创建string_pool.go文件,在其中定义internedStrings变量,代码如下:
package heap
import "unicode/utf16"
// 用map来表示字符串池,key是Go字符串,value是Java字符串
var internedStrings = map[string]*Object{}
// todo
// go string -> java.lang.String
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
}
// java.lang.String -> go string
func GoString(jStr *Object) string {
charArr := jStr.GetRefVar("value", "[C")
return utf16ToString(charArr.Chars())
}
// utf8 -> utf16
func stringToUtf16(s string) []uint16 {
runes := []rune(s) // utf32
return utf16.Encode(runes) // func Encode(s []rune) []uint16
}
// utf16 -> utf8
func utf16ToString(s []uint16) string {
runes := utf16.Decode(s) // func Decode(s []uint16) []rune
return string(runes)
}
代码解释
map
用map来表示字符串池,key是Go字符串,value是Java字符串。

JString函数
JString函数根据Go字符串返回相应的Java字符串实例。如果Java字符串已经在池中,直接返回即可,否则先把Go字符串(UTF8格式)转换成Java字符数组(UTF16格式),然后创建一个Java字符串实例,把它的value变量设置成刚刚转换而来的字符数组,最后把Java字符串放入池中。注意,这里其实是跳过了String的构造函数,直接用hack的方式创建实例。

stringToUtf16函数
Go语言字符串在内存中是UTF8编码的,先把它强制转成UTF32,然后调用utf16包的Encode()函数编码成UTF16。
rune是int32的别名,在所有方面都等同于int32。按照惯例,它被用来区分字符值和整数值。

rune是int32的别名,在所有方面都等同于int32。按照惯例,它被用来区分字符值和整数值。

扩展:
utf16 – The Go Programming Language (studygolang.com)
Encode()函数:Encode返回Unicode码点序列s的UTF-16编码。

Decode( )函数:Decode返回由UTF-16编码s表示的Unicode码点序列。

3.完善ldc指令
打开\instructions\constants\ldc.go文件,然后修改_ldc()函数,改动如下:
func _ldc(frame *rtda.Frame, index uint) {
stack := frame.OperandStack()
class := frame.Method().Class()
c := class.ConstantPool().GetConstant(index)
switch c.(type) {
case int32:
stack.PushInt(c.(int32))
case float32:
stack.PushFloat(c.(float32))
case string:
// 从字符串常量池中获取Java字符串
internedStr := heap.JString(class.Loader(), c.(string))
stack.PushRef(internedStr)
// case *heap.ClassRef:
// case MethodType, MethodHandle
default:
panic("todo: ldc!")
}
}如果ldc试图从运行时常量池中加载字符串常量,则先通过常量拿到Go字符串,然后把它转成Java字符串实例并把引用推入操作数栈顶。
4.完善类加载器
打开\rtda\heap\class_loader.go文件,修改initStaticFinalVar函数,改动如下:
case "Ljava/lang/String;":
goStr := cp.GetConstant(cpIndex).(string)
jStr := JString(class.Loader(), goStr)
vars.SetRef(slotId, jStr)替换掉原来的逻辑。放到此处:

这里增加了字符串类型静态常量的初始化逻辑。
5.测试字符串
打开\main.go文件,修改startJVM()函数。改动非常小,只是在调用interpret()函数时,把传递给Java主方法的参数传递给它,
代码如下:
interpret(mainMethod, cmd.verboseInstFlag, cmd.args)
打开interpreter.go文件,修改interpret()函数,interpret()函数接收从startJVM()函数中传递过来的args参数,然后调用createArgsArray()函数把它转换成Java字符串数组,最后把这个数组推入操作数栈顶。
func interpret(method *heap.Method, logInst bool, args []string) {
thread := rtda.NewThread()
frame := thread.NewFrame(method)
thread.PushFrame(frame)
// 把它转换成Java字符串数组
jArgs := createArgsArray(method.Class().Loader(), args)
// 将引用存储到局部变量表第0位
frame.LocalVars().SetRef(0, jArgs)
defer catchErr(thread)
loop(thread, logInst)
}
// 生成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
}
最后,打开instructions\references\invokevirtual.go,修改_println()函数,添加一个case,改动如下:
case "(Ljava/lang/String;)V":
jStr := stack.PopRef()
goStr := heap.GoString(jStr)
fmt.Println(goStr)
GoString()函数在/rtda/heap/string_pool.go文件文件中,先拿到String对象的value变量值,然后把字符数组转换成Go字符串。
// java.lang.String -> go string
func GoString(jStr *Object) string {
charArr := jStr.GetRefVar("value", "[C")
return utf16ToString(charArr.Chars())
}utf16ToString()函数在/rtda/heap/string_pool.go文件中,utf16 -> utf8,先把UTF16数据转换成UTF8编码,然后强制转换成Go字符串即可,代码如下:
// utf16 -> utf8
func utf16ToString(s []uint16) string {
runes := utf16.Decode(s) // func Decode(s []uint16) []rune
return string(runes)
}
著名的HelloWorld:
package jvmgo.book.ch01;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}go install jvmgo\ch08
ch08 -classpath D:\MAT_log -Xjre "D:\software\java\jre" HelloWorld

目前,我们并不是通过调用System.out.println()方法打印,而是通过hack的方式打印的。下面我们继续完善。
6.参考
尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ
周志明:深入理解java虚拟机
张秀宏:自己动手写Java虚拟机 (Java核心技术系列)


