8

Golang实现JAVA虚拟机-运行时数据区 - 橡皮筋儿

 8 months ago
source link: https://www.cnblogs.com/Gao-yubo/p/17925207.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

原文链接:https://gaoyubo.cn/blogs/8ae1f4ca.html

Golang实现JAVA虚拟机-解析class文件

一、运行时数据区概述

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

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

  • 多线程共享的运行时数据区需要在Java虚拟机启动时创建好,在Java虚拟机退出时销毁。
    • 对象实例存储在堆区
    • 类信息数据存储在方法区
    • 从逻辑上来讲,方法区其实也是堆的一部分。
  • 线程私有的运行时数据区则在创建线程时才创建,线程退出时销毁。
    • pc寄存器(Program Counter):执行java方法表示:正在执行的Java虚拟机指令的地址;执行本地方法:pc寄存器无意义
    • Java虚拟机栈(JVM Stack)。
      • 栈帧(Stack Frame),帧中保存方法执行的状态
        • 局部变量表(Local Variable):存放方法参数和方法内定义的局部变量。
        • 操作数栈(Operand Stack)等。

2319323-20231225033141289-754550482.png

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

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

二、数据类型概述

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

  • 基本类型的变量存放的就是数据本身
    • 布尔类型(boolean type)
    • 数字类型 (numeric type)
      • 整数类型(integral type)
      • 浮点数类型(floating-point type)。
  • 引用类型的变量存放的是对象引用,真正的对象数据是在堆里分配的。
    • 类型:指向类实例
    • 接口类型:用指向实现了该接口的类或数组实例
    • 数组类型: 指向数组实例
    • null:表示该引用不指向任何对 象。

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

2319323-20231225033141213-888829114.png

三、实现运行时数据区

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

package rtda
type Object struct {
	// todo
}

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

2319323-20231225033140622-1321049456.png

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

目前结构如下图:

2319323-20231225033140603-1475967779.png

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

弹出引用后,把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所示。

2319323-20231225033141056-383560361.png

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

2319323-20231225033141126-780172993.png

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

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

2319323-20231225033141216-750388502.png

fconst_2指令把2.0f推到栈顶

2319323-20231225033141270-429255450.png

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

2319323-20231225033141517-1382877368.png

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

2319323-20231225033141162-729183896.png

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

2319323-20231225033141468-404551468.png

fmul继续乘法计算

2319323-20231225033141677-1332691263.png

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

2319323-20231225033141607-756630915.png

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

2319323-20231225033141630-545819843.png

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

2319323-20231225033150218-1104339347.png

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK