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

一、运行时数据区概述

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 rtda
type Object struct {
	// todo
}

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

3.1线程

下创建**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()
}

3.2虚拟机栈

用经典的**链表(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
}

3.3栈帧

创建**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),
    }
}

目前结构如下图:

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 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值类来处理。

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

3.5操作数栈

操作数栈的实现方式和局部变量表类似。创建**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)
}

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

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

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

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

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

复制代码
00 ldc #4
02 fstore_1
03 fconst_2
04 fload_1
05 fmul
06 fload_0
07 fmul
08 fstore_2
09 fload_2
10 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

体验地址: 引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

相关推荐
学习编程的Kitty2 分钟前
算法——位运算
java·前端·算法
斑点鱼 SpotFish7 分钟前
用Python可视化国庆期间旅游概况与消费趋势
开发语言·python·旅游
only-lucky9 分钟前
在Qt中使用VTK
开发语言·qt
用户9047066835720 分钟前
如何使用 Spring MVC 实现 RESTful API 接口
java·后端
刘某某.21 分钟前
数组和小于等于k的最长子数组长度b
java·数据结构·算法
程序员飞哥26 分钟前
真正使用的超时关单策略是什么?
java·后端·面试
用户9047066835728 分钟前
SpringBoot 多环境配置与启动 banner 修改
java·后端
小杰帅气31 分钟前
类与对象1
开发语言·c++
chenyuhao202442 分钟前
《C++二叉引擎:STL风格搜索树实现与算法优化》
开发语言·数据结构·c++·后端·算法
小old弟1 小时前
后端三层架构
java·后端