bboyjing's blog

自己动手写JVM十一【运行时数据区(一)】

到目前为止,已经实现了搜索、解析class文件,本章节就来讨论下如何初步实现运行时数据区。

运行时数据区概述

在运行Java程序时,Java虚拟机需要使用内存在存放各式各样的数据。Java虚拟机规范把这些内存区域叫作运行时数据区。运行时数据区可以分成两类:一类是多线程共享的,另一类则是线程私有的。多线程共享的运行时数据去需要在Java虚拟机启动时创建浩,在Java虚拟机退出时销毁。线程私有的运行时数据区则是在线程创建时才创建,线程退出时销毁。
线程私有的运行时数据区用于辅助执行Java字节码。每个线程有自己的pc寄存器和Java虚拟机栈。Java虚拟机栈又由栈帧构成,栈帧中保存方法执行的状态,包括局部变量表和操作数栈等。本章节将开始学习如何初步实现线程私有的运行时数据区。

数据类型

Java虚拟机可以操作两类数据:基本类型和引用类型。基本类型存放的就是数据本身,引用类型的变量存放的是对象引用,真正的对象数据是在堆里分配的。基本类型可以进一步分为布尔类型和数字类型,数字类型又可以进一步分为整数类型和浮点数类型。引用类型可以进一步分为3种:类类型、接口类型和数组类型。类类型引用指向类实例,数组类型引用指向数组实例,接口类型引用指向实现了该接口的类或数组实例。引用类型有一个特殊的值–null,表示该引用不指向任何对象。
之前的章节已经说过了,Go语言提供了非常丰富的数据类型,可以对应Java的基本数据类型。对于引用类型,自然是选择使用指针,Go提供了nil,表示空指针,正好可以用来表示null。而且Go本身也有垃圾回收功能,所以可以直接使用Go的垃圾收集器。

实现运行时数据区

下面将开始实现线程私有的运行时数据区,相关代码位于rtdata包下。

线程

JVM中线程的构成如下:

1
2
3
4
5
6
7
JVM
Thread
pc
Stack
Frame
LocalVars
OperandStack

我们首先就来定义线程结构体,代码位于thread.go文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Thread结构体
type Thread struct {
// pc寄存器,存放当前正在执行的Java虚拟机指令的地址
pc int
// 虚拟机栈结构体指针
stack *Stack
}
// pc寄存器的Get、Set方法
func (self *Thread) PC() int {
return self.pc
}
func (self *Thread) SetPC(pc int) {
self.pc = pc
}

注:本章代码基本上都会引用到还没有讲到的数据结构,比如Stack结构体还没有定义,*Stack会编译不过,后面讲到的时候会补上,最终会编译通过。

Java虚拟机栈

Java虚拟机规范对Java虚拟机栈的约束相当宽松,Java虚拟机栈可以是连续的空间,也可以时不连续的;可以时固定大小的,也可以是运行时动态扩展的。如果Java虚拟机栈有大小限制,且执行线程所需要的栈空间超出了这个限制,会导致StackOverflowError异常抛出。如果Java虚拟机栈可以动态扩展,但是内存已经耗尽,会导致OutOfMemoryError异常抛出。我们用经典的链表数据结构来实现Java虚拟机栈,这样栈就可以按需使用内存空间,而且弹出的帧也可以及时被GO的垃圾收集器回收。下面定义Stack结构体,相关代码位于jvm_stack.go文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 虚拟机栈结构体
type Stack struct {
// 栈的容量(最多可以容纳多少帧)
maxSize uint
// 当前栈的大小
size uint
// 栈顶指针
_top *Frame
}
// 初始化虚拟机栈
func newStack(maxSize uint) *Stack {
return &Stack{maxSize:maxSize}
}

完善thread.go文件,添加初始化线程的方法,newStack()函数创建Stack结构体实例,参数表明Stack最多可以容纳多少栈帧。

1
2
3
4
5
6
// 初始化线程
func NewThread() *Thread {
return &Thread{
stack: newStack(1024),
}
}

栈帧

栈帧结构体位于frame.go文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Frame struct {
// 实现链表数据结构
lower *Frame
// 局部变量
localVars LocalVars
// 操作数栈指针
operandStack *OperandStack
}
// localVars和operandStackGet方法
func (self *Frame) LocalVars() LocalVars {
return self.localVars
}
func (self *Frame) OperandStack() *OperandStack {
return self.operandStack
}

由Thread、Stack和Frame的结构可以看出,Java虚拟机栈的链表结构如下图所示:
jvmgo_28

局部变量表

局部变量表时按索引访问的,所以很自然,可以把它想象成一个数组。根据Java虚拟机规范,这个数组的每个元素至少可以容纳一个int或引用之,两个连续的元素可以容纳一个long或double值。这里采用定义一个结构体来存放局部变量表,其结构体位于slot.go文件中:

1
2
3
4
5
6
type Slot struct {
// 整数
num int32
// 引用
ref *Object
}

num字段存放整数,ref字段存放引用,刚好满足需求。其中Object位于object.go文件中,暂时结构体中没有内容,其结构如下:

1
2
3
type Object struct {
}

下面就可以新建局部变量表结构体了,位于local_vars.go文件中:

1
2
3
4
5
6
7
8
9
10
11
// 局部变量表结构体
type LocalVars []Slot
// 创建LocalVars实例
func newLocalVars(maxLocals uint) LocalVars {
if maxLocals > 0 {
return make([]Slot, maxLocals)
}
return nil
}

继续编辑local_vars.go文件,给LocalVars类型定义一些方法,用来存取不同类型的变量。代码这里就不贴出来了,参照项目。顺便说一下,代码中没有给boolean、byte、short和char类型定义存取方法,这些类型的值都可以转换成int来处理。

操作数栈

操作数栈的实现方式和局部变量表雷系,其结构体位于operand_stack.go文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type OperandStack struct {
// 记录栈顶位置
size uint
// 操作数栈的大小时编译器已经确定的,所以可以用[]Slot实现
slots []Slot
}
// 创建OperandStack实例
func newOperandStack(maxStack uint) *OperandStack {
if maxStack > 0 {
return &OperandStack{
slots: make([]Slot, maxStack),
}
}
return nil
}

和局部变量表类似,需要定义一些方法从操作数栈中弹出或者推入各种类型的变量,这里依然不贴出代码了。

完善代码

至此,本章最基础单位的代码已经写完,下面来完善下之前没完成的代码。

  1. 完善栈帧

    1
    2
    3
    4
    5
    6
    7
    // 实例化栈帧
    func NewFrame(maxLocals, maxStack uint) *Frame {
    return &Frame{
    localVars: newLocalVars(maxLocals),
    operandStack: newOperandStack(maxStack),
    }
    }
  2. 完善虚拟机栈

    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
    // 返回栈顶指针
    func (self *Stack) top() *Frame {
    if self._top == nil {
    // 若栈时空的,肯定是有bug,需要panic
    panic("jvm stack is empty!")
    }
    return self._top
    }
    // 入栈
    func (self *Stack) push(frame *Frame) {
    // 超过栈最大深度报错
    if self.size > self.maxSize {
    panic("java.lang.StackOverflowError")
    }
    // 将栈顶指针下移
    if self._top != nil {
    frame.lower = self._top
    }
    self._top = frame
    self.size++
    }
    // 出栈
    func (self *Stack) pop() *Frame {
    // 若栈顶指针为空,报错
    if self._top == nil {
    panic("jvm stack is empty!")
    }
    top := self._top
    // 弹出栈顶指针
    self._top = top.lower
    // 弹出指针的lower指向不再有意义
    top.lower = nil
    self.size--
    return top;
    }
  3. 完善线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 入栈,调用虚拟机栈的方法
    func (self *Thread) PushFrame(frame *Frame) {
    self.stack.push(frame)
    }
    // 出栈,调用虚拟机栈的方法
    func (self *Thread) PopFrame() *Frame {
    return self.stack.pop()
    }
    // 查看栈顶指针,调用虚拟机栈的方法
    func (self *Thread) CurrentFrame() *Frame {
    return self.stack.top()
    }

本章节就到此结束,下一章来分析下局部变量表和操作数栈实例。