写点什么

Golang 实现 JAVA 虚拟机 - 运行时数据区

作者:EquatorCoco
  • 2023-12-25
    福建
  • 本文字数:6507 字

    阅读完需:约 21 分钟

一、运行时数据区概述


JVM 学习: JVM-运行时数据区


运行时数据区可以分为两类:一类是多线程共享的,另一类则是线程私有的。


  • 多线程共享的运行时数据区需要在 Java 虚拟机启动时创建好,在 Java 虚拟机退出时销毁。对象实例存储在堆区类信息数据存储在方法区从逻辑上来讲,方法区其实也是堆的一部分。


  • 线程私有的运行时数据区则在创建线程时才创建,线程退出时销毁。pc 寄存器(Program Counter):执行 java 方法表示:正在执行的 Java 虚拟机指令的地址;执行本地方法:pc 寄存器无意义 Java 虚拟机栈(JVM Stack)。栈帧(Stack Frame),帧中保存方法执行的状态局部变量表(Local Variable):存放方法参数和方法内定义的局部变量。操作数栈(Operand Stack)等。



虚拟机实现者可以使用任何垃圾回收算 法管理堆,甚至完全不进行垃圾收集也是可以的。


由于 Go 本身也有垃圾回收功能,所以可以直接使用 Go 的垃圾收集器,这大大简化了工作


二、数据类型概述


Java 虚拟机可以操作两类数据:基本类型(primitive type)和引用类型(reference type)。


  • 基本类型的变量存放的就是数据本身布尔类型(boolean type)数字类型 (numeric type)整数类型(integral type)浮点数类型(floating-point type)。


  • 引用类型的变量存放的是对象引用,真正的对象数据是在堆里分配的。类型:指向类实例接口类型:用指向实现了该接口的类或数组实例数组类型: 指向数组实例null:表示该引用不指向任何对 象。


对于基本类型,可以直接在 Go 和 Java 之间建立映射关系。对于引用类型,自然的选择是使用指针。Go 提供了 nil,表示空指针,正好可以用来表示 null。



三、实现运行时数据区


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


package rtdatype Object struct {	// todo}
复制代码


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



3.1 线程


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


package rtdatype Thread struct {	pc int	stack *Stack}func NewThread() *Thread {...}func (self *Thread) PC() int { return self.pc } // getterfunc (self *Thread) SetPC(pc int) { self.pc = pc } // setterfunc (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()}
复制代码


3.2 虚拟机栈


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


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


package rtdatype 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}
复制代码


3.3 栈帧


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


package rtdatype 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),    }}
复制代码


目前结构如下图:



3.4 局部变量表


局部变量表的容量以变量槽(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 rtdaimport "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 slotsfunc (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 slotsfunc (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 值类来处理。


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


3.5 操作数栈


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


package rtdaimport "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 占两个soltfunc (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 slotsfunc (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)}
复制代码


// double consumes two slotsfunc (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}
复制代码


四、局部变量表和操作数栈实例分析


以圆形的周长公式为例进行分析,下面是 Java 方法的代码。


public static float circumference(float r) {    float pi = 3.14f;    float area = 2 * pi * r;    return area;}
复制代码


上面的方法会被javac编译器编译成如下字节码:


00 ldc #402 fstore_103 fconst_204 fload_105 fmul06 fload_007 fmul08 fstore_209 fload_210 return
复制代码


下面分析这段字节码的执行。


circumference()方法的局部变量表大小是 3,操作数栈深度是 2。假设调用方法时,传递给它的参数 是 1.6f,方法开始执行前,帧的状态如图 4-3 所示。



第一条指令是ldc,它把 3.14f 推入栈顶



上面是局部变量表和操作数栈过去的状态,最下面是当前状态。


接着是fstore_1指令,它把栈顶的 3.14f 弹出,放到 #1 号局部变量中



fconst_2指令把 2.0f 推到栈顶



fload_1指令把 #1 号局部变量推入栈顶



fmul指令执行浮点数乘法。它把栈顶的两个浮点数弹出,相乘,然后把结果推入栈顶



fload_0指令把 #0 号局部变量推入栈顶



fmul继续乘法计算



fstore_2指令把操作数栈顶的 float 值弹出,放入 #2 号局部变量表



最后freturn指令把操作数栈顶的 float 变量弹出,返回给方法调 用者



五、测试


main()方法中修改 startJVM:


func startJVM(cmd *Cmd) {    frame := rtda.NewFrame(100, 100)    testLocalVars(frame.LocalVars())    testOperandStack(frame.OperandStack())}
func testLocalVars(vars rtda.LocalVars) { vars.SetInt(0, 100) vars.SetInt(1, -100) vars.SetLong(2, 2997924580) vars.SetLong(4, -2997924580) vars.SetFloat(6, 3.1415926) vars.SetDouble(7, 2.71828182845) vars.SetRef(9, nil) println(vars.GetInt(0)) println(vars.GetInt(1)) println(vars.GetLong(2)) println(vars.GetLong(4)) println(vars.GetFloat(6)) println(vars.GetDouble(7)) println(vars.GetRef(9))}
func testOperandStack(ops *rtda.OperandStack) { ops.PushInt(100) ops.PushInt(-100) ops.PushLong(2997924580) ops.PushLong(-2997924580) ops.PushFloat(3.1415926) ops.PushDouble(2.71828182845) ops.PushRef(nil) println(ops.PopRef()) println(ops.PopDouble()) println(ops.PopFloat()) println(ops.PopLong()) println(ops.PopLong()) println(ops.PopInt()) println(ops.PopInt())}
复制代码



文章转载自:橡皮筋儿

原文链接:https://www.cnblogs.com/Gao-yubo/p/17925207.html

体验地址:http://www.jnpfsoft.com/?from=001


用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
Golang实现JAVA虚拟机-运行时数据区_Java_EquatorCoco_InfoQ写作社区