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


    原文链接: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)等。

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

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

    弹出引用后,把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())
    }
    

    __EOF__

  • 本文作者: 疼了也不哭
  • 本文链接: https://www.cnblogs.com/Gao-yubo/p/17925207.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    优秀公共DNS服务器推荐
    基于springboot,vue图书管理系统
    Java面向对象(基础)-- 类的成员之三:构造器(Constructor)
    【分布式】入门级NCCL多机并行实践 - 02
    0x04_自制操作系统My-OS实现自定义颜色
    反距离加权插值IDW计算详细步骤
    7-2 摘花生
    机械臂速成小指南(十九):机械臂的电路板抓取实验
    Elasticsearch 分片内部原理—近实时搜索、持久化变更
    java中的反射机制
  • 原文地址:https://www.cnblogs.com/Gao-yubo/p/17925207.html