bboyjing's blog

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

在大部分编程语言中,数组和字符串都是最基本的数据类型。Java虚拟机直接支持数组,对字符串的支持则由java.lang.String和相关的类提供。本章就来学习数组和字符串相关指令。

数组概述

数组在Java虚拟机中是个比较特殊的概念。为什么这么说呢,有下面几个原因:

  1. 数组类和普通的类是不同的。普通的类从class文件中加载,但是数组类由Java虚拟机在运行时生成。数组的类名是左方括号([)+数组元素的类型描述符;数组类型描述符就是类名本身。例如int[]的类名是[I,int[][]的类名是[[I,Object[]的类名是[Ljava/lang/Object;,String[][]的类名是[[Ljava/lang/String;,等等。
  2. 创建数组的方式和创建普通对象的方式不同。普通对象由new指令创建,然后由构造函数初始化。基本类型数组由newarray指令创建;引用类型数组由anewarray指令创建;另外还有一个专门的multianewarray指令用于创建多维数组。
  3. 数组和普通对象存放的数据叶不同。普通对象中存放的是实例变量,通过putfield和各条field指令存取。数组对象中存放的则是数组元素,通过<t>aload和<t>astore系列指令按索引存取。其中<t>可以是a、b、c、d、f、i、l或者s。分别用于存取引用、byte、char、double、float、int、long或short类型的数组。

数组实现

将在类和对象的基础上实现数组类和数组对象。

数组对象

和普通对象一样,数组也是分配在堆中,通过引用来使用。所以需要改造Object结构体,让它既可以表示普通对象,也可以表示数组。修改/rtdata/heap/object.go文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Object结构体
type Object struct {
// 对象的Class指针
class *Class
// 实例变量,可以容纳任何类型的值
data interface{}
}
func newObject(class *Class) *Object {
return &Object{
class: class,
data: newSlots(class.instanceSlotCount),
}
}
// Fields()方法仍然只针对普通对象,转成Slots
func (self *Object) Fields() Slots {
return self.data.(Slots)
}

需要给Object结构体添加几个数组特有的方法,单独定义在/rtdata/heap/array_object.go文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 获取类型为byte的数组
func (self *Object) Bytes() []int8 {
return self.data.([]int8)
}
// 省略获取其他类型的数组
...
// 获取数组长度
func (self *Object) ArrayLength() int32 {
switch self.data.(type) {
case []int8:
return int32(len(self.data.([]int8)))
// 省略其他类型
...
default:
panic("Not array!")
}
}

数组类

不需要修改Class结构体,只要给它添加几个数组特有的方法即可。为了强调这些方法只是针对数组类,把这些方法定义到新的文件/rtdata/heap/array_class.go中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 判断是否是数组类
func (self *Class) IsArray() bool {
return self.name[0] == '['
}
// 创建数组
func (self *Class) NewArray(count uint) *Object {
if !self.IsArray() {
panic("Not array class: " + self.name)
}
switch self.Name() {
// 省略其他类型
...
default:
return &Object{self, make([]*Object, count)}
}
}

有一点要说明下,上面都没看到boolean类型的身影,因为使用的是[]int8来表示布尔数组。

加载数组类

修改/rtdata/heap/class_loader.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
36
37
38
// 把类数据加载到方法区
func (self *ClassLoader) LoadClass(name string) *Class {
...
// 若类名的第一个字符是'[',表示该类是数组类
if name[0] == '[' {
return self.loadArrayClass(name)
}
...
}
// 加载数组类
func (self *ClassLoader) loadArrayClass(name string) *Class {
/*
生成Class结构体,数组类由Java虚拟机在运行时生成
所以没有像loadNonArrayClass()那样读取class文件
*/
class := &Class{
// 访问标识
accessFlags: ACC_PUBLIC, // todo
// 类名
name: name,
// 类加载器
loader: self,
// 数组类不需要初始化,所以设成true
initStarted: true,
// 数组类的超类是java/lang/Object
superClass: self.LoadClass("java/lang/Object"),
interfaces: []*Class{
// 数组类实现了java.lang.Cloneable接口
self.LoadClass("java/lang/Cloneable"),
// 数组类实现了java.io.Serializable
self.LoadClass("java/io/Serializable"),
},
}
// 记录该数组类已加载
self.classMap[name] = class
return class
}

数组相关指令

下面将要实现20条指令,其中newarray、anewarray、multianewarray和arraylength指令属于引用类指令;<t>aload和<t>astore系列各有8条,分别属于加载类和存储类指令。下面在java项目中新建ArrayDamo.java,后面就拿该类做测试。

1
2
3
4
5
6
7
8
9
10
11
12
public class ArrayDemo {
public static void main(String[] args) {
int[] a1 = new int[10]; // newarray
String[] a2 = new String[10]; // anewarray
int[][] a3 = new int[10][10]; // multianewarray
int x = a1.length; // arraylength
a1[0] = 100; // iastore
int y = a1[0]; // iaload
a2[0] = "abc"; // aastore
String s = a2[0]; // aaload
}
}

newarray指令

newarray指令用来创建基本类型数组,包括boolean[]、byte[]、char[]、short[]、int[]、long[]、float[]、double[]8种。其实现代码位于/instructions/references/newarray.go文件中:

1
2
3
4
// newarray指令结构体
type NEW_ARRAY struct {
atype uint8
}

newarray指令需要两个操作数。第一个操作数时一个uint8整数,在字节码中紧跟在指令操作码后面,表示要创建哪种类型的数组。Java虚拟机规范把这个操作数叫座atype,并且规定了它的有效值,在newarray.go文件中定义常量和读取第一个操作数的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// atype常量,对应8种基本类型
const (
AT_BOOLEAN = 4
AT_CHAR = 5
AT_FLOAT = 6
AT_DOUBLE = 7
AT_BYTE = 8
AT_SHORT = 9
AT_INT = 10
AT_LONG = 11
)
// 读取第一个单字节操作数
func (self *NEW_ARRAY) FetchOperands(reader *base.BytecodeReader) {
self.atype = reader.ReadUint8()
}

newarray指令的第二个操作数是count,从操作数栈中弹出,表示数组长度。在newarray.go文件中定义Execute()方法,在实现该方法之前要先修改下/rtdata/heap/class.go文件,添加获取当前class的ClassLoader,很简单,就一个普通的Getter方法,这里就不贴出来了。

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
func (self *NEW_ARRAY) Execute(frame *rtdata.Frame) {
// 获取当前帧的操作数栈
stack := frame.OperandStack()
// 弹出栈顶元素,作为数组的长度
count := stack.PopInt()
if count < 0 {
panic("java.lang.NegativeArraySizeException")
}
// 获取当前类的类加载器
classLoader := frame.Method().Class().Loader()
// 加载数组类
arrClass := getPrimitiveArrayClass(classLoader, self.atype)
// 创建数组
arr := arrClass.NewArray(uint(count))
// 将创建的数组指针推入操作数栈
stack.PushRef(arr)
}
// 根据atype加载对应的Class
func getPrimitiveArrayClass(loader *heap.ClassLoader, atype uint8) *heap.Class {
switch atype {
case AT_BOOLEAN:
return loader.LoadClass("[Z")
// 省略其他类型的case
...
default:
panic("Invalid atype!")
}
}

anewarray指令

anewarray指令用来创建引用类型数组,其实现代码位于/instructions/references/anewarray.go文件中。anewarray指令也需要两个操作数。第一个操作数时uint16索引,来自字节码。通过这个索引可以从当前类的运行时常量池中找到一个类符号索引,解析这个符号引用就可以得到数组元素的类。第二个操作数是数组的长度,从操作数栈中弹出。在实现该指令之前,也要做些准备工作,下面先来看下:

  1. 将类型名转成类型描述符,新建/rtdata/heap/class_name_helper.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
    36
    37
    38
    39
    40
    41
    42
    43
    /*
    将类名转换成类型描述符
    XXX -> [LXXX;
    int -> [I
    [XXX -> [[XXX
    */
    func getArrayClassName(className string) string {
    return "[" + toDescriptor(className)
    }
    /*
    将类名转成类型描述符
    XXX => LXXX;
    int => I
    [XXX => [XXX
    */
    func toDescriptor(className string) string {
    // 如果已经是数组类名,直接返回
    if className[0] == '[' {
    // array
    return className
    }
    // 如果是基本类型名,返回对应的类型描述符
    if d, ok := primitiveTypes[className]; ok {
    // primitive
    return d
    }
    // 普通类名转成类型描述符L***;
    return "L" + className + ";"
    }
    // 基本类型和对应类型描述符map
    var primitiveTypes = map[string]string{
    "void": "V",
    "boolean": "Z",
    "byte": "B",
    "short": "S",
    "int": "I",
    "long": "J",
    "char": "C",
    "float": "F",
    "double": "D",
    }
  2. 修改/rtdata/heap/class.go文件,添加加载数组类的方法:

    1
    2
    3
    4
    5
    6
    func (self *Class) ArrayClass() *Class {
    // 获取数组类名
    arrayClassName := getArrayClassName(self.name)
    // 通过数组类名加载该数组类
    return self.loader.LoadClass(arrayClassName)
    }

准备工作做好,我们来实现anewarray指令:

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
// anewarray指令结构体
type ANEW_ARRAY struct{
base.Index16Instruction
}
func (self *ANEW_ARRAY) Execute(frame *rtdata.Frame) {
// 获取运行时常量池
cp := frame.Method().Class().ConstantPool()
// 获取类符号引用
classRef := cp.GetConstant(self.Index).(*heap.ClassRef)
// 解析类符号引用
componentClass := classRef.ResolvedClass()
stack := frame.OperandStack()
// 从操作数栈中弹出元素,作为数组长度
count := stack.PopInt()
if count < 0 {
panic("java.lang.NegativeArraySizeException")
}
// 加载数组类
arrClass := componentClass.ArrayClass()
// 创建数组
arr := arrClass.NewArray(uint(count))
// 将数组指针推入操作数栈
stack.PushRef(arr)
}

arraylength指令

arraylength指令用于获取数组长度,实现代码位于/instructions/references/arraylength.go文件中,该指令只需要一个操作数,即从操作数栈顶弹出的数组引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// arraylength指令结构体
type ARRAY_LENGTH struct{ base.NoOperandsInstruction }
func (self *ARRAY_LENGTH) Execute(frame *rtdata.Frame) {
// 获取操作数栈,并且弹出栈顶引用
stack := frame.OperandStack()
arrRef := stack.PopRef()
// 若引用为空,抛出NullPointerException异常
if arrRef == nil {
panic("java.lang.NullPointerException")
}
// 获取数组长度
arrLen := arrRef.ArrayLength()
// 讲数组长度推入操作数栈顶
stack.PushInt(arrLen)
}