动手实现java虚拟机(四)实现运行时数据区

134 阅读6分钟

运行时数据区概述

java虚拟机运行java程序时,需要使用内存来存放各式各样的数据。虚拟机规范把这些内存区域称之为运行时数据区。分为两类,一类是多线程共享的,另一类是线程私有的。多线程共享的运行时数据区需要再java虚拟机启动时创建好,在java虚拟机退出时销毁。线程私有的运行时数据区在创建线程时才创建,线程退出时销毁。
多线程共享的内存区域主要存放两类数据:类数据和类实例(对象)。对象数据存放在堆中,类数据存放在方法区中。堆由垃圾收集器定期清理,程序员不需要考虑对象空间的释放。类数据包括字段和方法信息、方法的字节码、运行时常量池等。
线程私有的运行时数据区用于辅助执行java字节码。每个线程都有自己的pc寄存器和java虚拟机栈。虚拟机栈由栈帧构成,帧中保存方法执行的状态,包括局部变量表和操作数栈等

运行时数据区的结构如下
rtda.png Go自带gc,所以直接使用Go的堆和垃圾回收器简化工作

数据类型

java虚拟机可以操作两类数据:基本类型和引用类型。基本变量存放的是数据本身,引用类型的变量存放的是对象引用,真正的对象数据是在堆里分配的。
基本类型可以分为布尔类型和数字类型,数字类型又可以分为整数类型和浮点数类型。引用类型分为三种:类类型、接口类型、数组类型。类类型引用指向类实例,数组类型引用指向数组实例,接口实例引用指向实现了该接口的类或数组实例。
Go提供了丰富的数据类型,对于基本类型,可以直接在Go和java之间建立映射关系。对于引用类型,选择指针,Go提供nil用于表示空指针,正好用来表示null。

jvmda.png

java虚拟机数据类型

实现运行时数据区

线程

定义Thread结构体

type Thread struct {
	pc    int
	stack *Stack
}

stack字段是Stack(java虚拟机栈)结构体指针 创建Thread实例的方法(栈大小暂时为1024)

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

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

另外定义如下三个方法
PushFrame()和PopFrame()方法调用Stack结构体相应的方法

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

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

CurrentFrme()放回当前帧

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

java虚拟机栈

虚拟机栈结构体定义如下

type Stack struct {
	maxSize uint
	size    uint
	_top    *Frame
}

maxSize字段保存栈的容量,size字段保存栈的当前大小,_top字段保存栈顶指针
此外,定义一下几个方法

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

push()方法把帧压入栈中 如果栈已经满了,按照虚拟机规范应该跑出StackOverflowError异常,这里先暂时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

}

func (self *Stack) top() *Frame {
	if self._top == nil {
		panic("jvm stack is empty!")
	}

	return self._top
}

定义帧结构体,代码如下

type Frame struct {
	lower        *Frame
	localVars    LocalVars
	operandStack *OperandStack
}

lower字段用来实现栈的链表结构,localVars字段保存局部表量表指针,operandStack字段保存操作数栈指针 newFrame方法创建Frame实例

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

stack.png

使用链表实现java虚拟机栈

局部变量表

局部变量表是按索引访问的,容易想到使用数组实现。根据java虚拟机规范,这个数组的每个元素至少可以容纳一个int或引用值,连续的两个元素可以容纳一个long或者double
这里使用一个结构体数组来表示局部变量变,使他可以同时表示一个int值或一个引用值
结构体定义

type Slot struct {
	num int32
	ref *Object
}

num字段存放整数,ref字段存放引用 同样的,定义newLocalVars()方法创建实例,这里的maxLocals表示局部变量表的大小,由编译器预先计算好,代表方法运行时需要保存的最大局部变量数量

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

操作局部变量表和操作数栈的指令都是隐含类型的,下面定义一下方法,用来存取不同类型变量,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变量

func (self LocalVars) SetLong(index uint, val int64) {
	self[index].num = int32(val)
	self[index+1].num = int32(val >> 32)
}

func (self LocalVars) GetLong(index uint) int64 {
	low := uint32(self[index].num)
	high := uint32(self[index].num)
	return int64(high)<<32 | int64(low)
}

double变量先转成long类型,再按照long变量处理

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

引用类型直接存取即可,对于boolean、byte、shot和char类型可以转出int来处理。

操作数栈

定义操作数栈结构体OperandStack,代码如下

type OperandStack struct {
	size  uint
	slots []Slot
}

操作数栈的大小是编译期已经确定的,所以可以用[]slot实现,size字段用于记录栈顶位置,定义newOperandStack方法创建实例。 和局部变量表类似,定义一些方法操作操作数栈。

先看最简单的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
}

float变量需要转为int,long和double都拆成两个int变量在操作 引用类型:

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
}

弹出引用后,将Slot结构体的ref字段设置为nil,帮助垃圾回收

参考资料:
《自己动手写Java虚拟机》