跳至主要內容

实现运行时数据区

Mr.Hope...大约 8 分钟

创建\rtda目录(run-time data area),创建object.go文件, 在其中定义Object结构体,代码如下:

package rtda
type Object struct {
	// todo
}

本节将实现线程私有的运行时数据区,如下图。下面先从线程开始。

线程

下创建thread.go文件,在其中定义Thread结构体,代码如下:

package rtda
type Thread struct {
	pc int
	stack *Stack
}
func NewThread() *Thread {...}
func (self *Thread) PC() int { return self.pc } // getter
func (self *Thread) SetPC(pc int) { self.pc = pc } // setter
func (self *Thread) PushFrame(frame *Frame) {...}
func (self *Thread) PopFrame() *Frame {...}
func (self *Thread) CurrentFrame() *Frame {...}

目前只定义了pc和stack两个字段。

  • pc字段代表(pc寄存器)
  • stack字段是Stack结构体(Java虚拟机栈)指针

和堆一样,Java虚拟机规范对Java虚拟机栈的约束也相当宽松。 Java虚拟机栈可以是:连续的空间,也可以不连续;可以是固定大小,也可以在运行时动态扩展。

  • 如果Java虚拟机栈有大小限制, 且执行线程所需的栈空间超出了这个限制,会导致 StackOverflowError异常抛出。
  • 如果Java虚拟机栈可以动态扩展,但 是内存已经耗尽,会导致OutOfMemoryError异常抛出。

创建Thread实例的代码如下:

func NewThread() *Thread {
	return &Thread{
		stack: newStack(1024),
	}
}

newStack()函数创建Stack结构体实例,它的参数表示要创建的Stack最多可以容纳多少帧

PushFrame()PopFrame()方法只是调用Stack结构体的相应方法而已,代码如下:

func (self *Thread) PushFrame(frame *Frame) {
    self.stack.push(frame)
}
func (self *Thread) PopFrame() *Frame {
    return self.stack.pop()
}

CurrentFrame()方法返回当前帧,代码如下:

func (self *Thread) CurrentFrame() *Frame {
	return self.stack.top()
}

虚拟机栈

用经典的链表(linked list)数据结构来实现Java虚拟机栈,这样就可以按需使用内存空间,而且弹出的也可以及时被Go的垃圾收集器回收。

创建jvm_stack.go文件,在其中定义Stack结构体,代码如下:

package rtda
type Stack struct {
    maxSize uint
    size uint
    _top *Frame
}
func newStack(maxSize uint) *Stack {...}
func (self *Stack) push(frame *Frame) {...}
func (self *Stack) pop() *Frame {...}
func (self *Stack) top() *Frame {...}

maxSize字段保存栈的容量(最多可以容纳多少帧),size字段保存栈的当前大小,_top字段保存栈顶指针。newStack()函数的代码 如下:

func newStack(maxSize uint) *Stack {
    return &Stack{
       maxSize: maxSize,
    }
}

push()方法把帧推入栈顶,目前没有实现异常处理,采用panic代替,代码如下:

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++
}

pop()方法把栈顶帧弹出:

func (self *Stack) pop() *Frame {
    if self._top == nil {
       panic("jvm stack is empty!")
    }
    //取出栈顶元素
    top := self._top
    //将当前栈顶的下一个栈帧作为栈顶元素
    self._top = top.lower
    //取消链表链接,将栈顶元素分离
    top.lower = nil
    self.size--

    return top
}

top()方法查看栈顶栈帧,代码如下:

// 查看栈顶元素
func (self *Stack) top() *Frame {
    if self._top == nil {
       panic("jvm stack is empty!")
    }

    return self._top
}

栈帧

创建frame.go文件,在其中定义Frame结构体,代码如下:

package rtda
type Frame struct {
    lower *Frame               //指向下一栈帧
	localVars    LocalVars     // 局部变量表
	operandStack *OperandStack //操作数栈
}
func newFrame(maxLocals, maxStack uint) *Frame {...}

Frame结构体暂时也比较简单,只有三个字段,后续还会继续完善它。

  • lower字段用来实现链表数据结构
  • localVars字段保存局部变量表指针
  • operandStack字段保存操作数栈指针

NewFrame()函数创建Frame实例,代码如下:

func NewFrame(maxLocals, maxStack uint) *Frame {
    return &Frame{
       localVars:    newLocalVars(maxLocals),
       operandStack: newOperandStack(maxStack),
    }
}

目前结构如下图:

局部变量表

局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。

在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。(最大Slot数量)

局部变量表是按索引访问的,所以很自然,可以把它想象成一 个数组。

根据Java虚拟机规范,这个数组的每个元素至少可以容纳 一个int或引用值,两个连续的元素可以容纳一个long或double值。 那么使用哪种Go语言数据类型来表示这个数组呢? 最容易想到的是[]int。Go的int类型因平台而异,在64位系统上是int64,在32 位系统上是int32,总之足够容纳Java的int类型。另外它和内置的uintptr类型宽度一样,所以也足够放下一个内存地址。

通过unsafe包可以拿到结构体实例的地址,如下所示:

obj := &Object{}
ptr := uintptr(unsafe.Pointer(obj))
ref := int(ptr)

但Go的垃圾回收机制并不能有效处理uintptr指针。 也就是说,如果一个结构体实例,除了uintptr类型指针保存它的地址之外,其他地方都没有引用这个实例,它就会被当作垃圾回收。

另外一个方案是用[]interface{}类型,这个方案在实现上没有问题,只是写出来的代码可读性太差。

第三种方案是定义一个结构体,让它可以同时容纳一个int值和一个引用值。

这里将使用第三种方案。创建slot.go文件,在其中定义Slot结构体, 代码如下:

package rtda

type Slot struct {
	num int32
	ref *Object
}

num字段存放整数,ref字段存放引用,刚好满足我们的需求。

用它来实现局部变量表。创建local_vars.go文件,在其中定义LocalVars类型,代码如下:

package rtda
import "math"
type LocalVars []Slot

定义newLocalVars()函数, 代码如下:

func newLocalVars(maxLocals uint) LocalVars {
    if maxLocals > 0 {
       return make([]Slot, maxLocals)
    }
    return nil
}

操作局部变量表和操作数栈的指令都是隐含类型信息的。下面给LocalVars类型定义一些方法,用来存取不同类型的变量。 int变量最简单,直接存取即可

func (self LocalVars) SetInt(index uint, val int32) {
    self[index].num = val
}
func (self LocalVars) GetInt(index uint) int32 {
    return self[index].num
}

float变量可以先转成int类型,然后按int变量来处理。

func (self LocalVars) SetFloat(index uint, val float32) {
    bits := math.Float32bits(val)
    self[index].num = int32(bits)
}
func (self LocalVars) GetFloat(index uint) float32 {
    bits := uint32(self[index].num)
    return math.Float32frombits(bits)
}

long变量则需要拆成两个int变量。(用两个slot存储)

// long consumes two slots
func (self LocalVars) SetLong(index uint, val int64) {
    //后32位
    self[index].num = int32(val)
    //前32位
    self[index+1].num = int32(val >> 32)
}
func (self LocalVars) GetLong(index uint) int64 {
    low := uint32(self[index].num)
    high := uint32(self[index+1].num)
    //拼在一起
    return int64(high)<<32 | int64(low)
}

double变量可以先转成long类型,然后按照long变量来处理。

// double consumes two slots
func (self LocalVars) SetDouble(index uint, val float64) {
    bits := math.Float64bits(val)
    self.SetLong(index, int64(bits))
}
func (self LocalVars) GetDouble(index uint) float64 {
    bits := uint64(self.GetLong(index))
    return math.Float64frombits(bits)
}

最后是引用值,也比较简单,直接存取即可。

func (self LocalVars) SetRef(index uint, ref *Object) {
    self[index].ref = ref
}
func (self LocalVars) GetRef(index uint) *Object {
    return self[index].ref
}

注意,并没有真的对boolean、byte、short和char类型定义存取方法,这些类型的值都可以转换成int值类来处理。

下面我们来实现操作数栈。

操作数栈

操作数栈的实现方式和局部变量表类似。创建operand_stack.go文件,在其中定义OperandStack结构体,代码如下:

package rtda
import "math"
type OperandStack struct {
    size uint
    slots []Slot
}

操作数栈的大小是编译器已经确定的,所以可以用[]Slot实现。 size字段用于记录栈顶位置。 实现newOperandStack()函数,代码如下:

func newOperandStack(maxStack uint) *OperandStack {
	if maxStack > 0 {
		return &OperandStack{
			slots: make([]Slot, maxStack),
		}
	}
	return nil
}

需要定义一些方法从操作数栈中弹出,或者往其中推入各种类型的变 量。首先实现最简单的int变量。

func (self *OperandStack) PushInt(val int32) {
    self.slots[self.size].num = val
    self.size++
}
func (self *OperandStack) PopInt() int32 {
    self.size--
    return self.slots[self.size].num
}

PushInt()方法往栈顶放一个int变量,然后把size加1。 PopInt() 方法则恰好相反,先把size减1,然后返回变量值。

float变量还是先转成int类型,然后按int变量处理。

func (self *OperandStack) PushFloat(val float32) {
    bits := math.Float32bits(val)
    self.slots[self.size].num = int32(bits)
    self.size++
}
func (self *OperandStack) PopFloat() float32 {
    self.size--
    bits := uint32(self.slots[self.size].num)
    return math.Float32frombits(bits)
}

把long变量推入栈顶时,要拆成两个int变量。 弹出时,先弹出 两个int变量,然后组装成一个long变量。

// long 占两个solt
func (self *OperandStack) PushLong(val int64) {
    self.slots[self.size].num = int32(val)
    self.slots[self.size+1].num = int32(val >> 32)
    self.size += 2
}
func (self *OperandStack) PopLong() int64 {
    self.size -= 2
    low := uint32(self.slots[self.size].num)
    high := uint32(self.slots[self.size+1].num)
    return int64(high)<<32 | int64(low)
}

double变量先转成long类型,然后按long变量处理。

// double consumes two slots
func (self *OperandStack) PushDouble(val float64) {
    bits := math.Float64bits(val)
    self.PushLong(int64(bits))
}
func (self *OperandStack) PopDouble() float64 {
    bits := uint64(self.PopLong())
    return math.Float64frombits(bits)
}

弹出引用后,把Slot结构体的ref字段设置成nil,这样做是为了帮助Go的垃圾收集器回收Object结构体实例。

func (self *OperandStack) PushRef(ref *Object) {
    self.slots[self.size].ref = ref
    self.size++
}
func (self *OperandStack) PopRef() *Object {
    self.size--
    ref := self.slots[self.size].ref
    //实现垃圾回收
    self.slots[self.size].ref = nil
    return ref
}