手写JVM(二十五)-字符串处理

代码、内容参考来自于张秀宏大佬的自己动手写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核心技术系列)

GO语言官网:Standard library – Go Packages

Java虚拟机规范:Chapter 4. The class File Format (oracle.com)

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇