原文链接: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())
}

Golang实现JAVA虚拟机-运行时数据区的更多相关文章

  1. 《深入理解Java虚拟机》(二)Java虚拟机运行时数据区

    Java虚拟机运行时数据区 详解 2.1 概述 本文参考的是周志明的 <深入理解Java虚拟机>第二章 ,为了整理思路,简单记录一下,方便后期查阅. 2.2 运行时数据区域 Java虚拟机 ...

  2. Java虚拟机运行时数据区

    运行时数据区程序计数器Java虚拟机栈本地方法栈Java堆(GC堆)方法区运行时常量池 运行时数据区 Java虚拟机在运行Java程序时,会将它所管理的内存划分为若干个内存区域.这些数据区域有各自的用 ...

  3. 面试常问的 Java 虚拟机运行时数据区

    写在前面 本文描述的有关于 JVM 的运行时数据区是基于 HotSpot 虚拟机. 概述 JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以 ...

  4. Java 虚拟机运行时数据区

    写在前面 本文描述的有关于 JVM 的运行时数据区是基于 HotSpot 虚拟机. 概述 JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以 ...

  5. 【深入理解Java虚拟机】Java虚拟机运行时数据区

    Java虚拟机运行时数据区 线程私有 程序计数器 1.当前线程所执行的字节码的行号指示器. 2.唯一不会发生OutOfMemoryError的区域 3.如果执行的是java方法,计数器值为虚拟机字节码 ...

  6. 笔记:Java虚拟机运行时数据区

    Java虚拟机在执行Java程序的过程中会把它管的内存划分为以下若干个不同的区域: 1.程序计数器 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器:由于Java虚拟机的 ...

  7. 【JVM从小白学成大佬】2.Java虚拟机运行时数据区

    目录 1.运行时数据区介绍 2.堆(Heap) 是否可能有两个对象共用一段内存的事故? 3.方法区(Method Area) 4.程序计数器(Program Counter Register) 5.虚 ...

  8. 【JVM学习】2.Java虚拟机运行时数据区

    来源: 公众号: 猿人谷 这里我们先说句题外话,相信大家在面试中经常被问到介绍Java内存模型,我在面试别人时也会经常问这个问题.但是,往往都会令我比较尴尬,我还话音未落,面试者就会"背诵& ...

  9. Java 虚拟机运行时数据区详解

    本文摘自深入理解 Java 虚拟机第三版 概述 Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟 ...

  10. 深入理解Java虚拟机&运行时数据区

      其中,程序计数器.虚拟机栈.本地方法栈3个区域随线程而生,随线程而灭.

随机推荐

  1. WPF学习 - 动画基础(1)

    1. WPF中的动画(Animation),是一种属性动画.技术上来说,它是让属性从一个值,变化到另一个值的过程.因此,有两条重要的特性: 1.1 只能为依赖属性应用动画(因为第二条特性). 1.2 ...

  2. QA|不同模块之间的引用(导入问题)问题;|Pycharm

    结构如图,在xxu的test.py中想要导入t2包中的sayhello和word两个方法 注意:首先需要打开xxu和t2的上层目录,因为解释器是从打开的那个文件开始查找的,所以这里应该打开B01_01 ...

  3. Netty+WebSocket整合STOMP协议

    1.STOMP协议简介 常用的WebSocket协议定义了两种传输信息类型:文本信息和二进制信息.类型虽然被确定,但是他们的传输体是没有规定的,也就是说传输体可以自定义成什么样的数据格式都行,只要客户 ...

  4. 什么是DCloud

    什么是DCloud1.什么是Dcloud2.主要包括 1. 开发工具 2. 前端框架 3. uniCloud 4. 5+app 5. MUI 6. wap2app1.什么是Dcloud 1. Dclo ...

  5. Windows11如何设置经典的右键菜单

    使用Windows11几个月了,解决了我的电脑经常性彻底死机.蓝屏的问题,系统也流畅.易用了好多.唯一不能忍受的是右键菜单,经常需要再点一次才能找到自己想要的选项,今天网搜了下解决办法,特记录于此. ...

  6. python制作定时发送信息脚本

    文章中提到的菜单是右下角这个 需求 我们需要做到打开微信获取输入框焦点及输入 思路 1,获取到右下角菜单的坐标和菜单中微信的坐标以及输入框的坐标 2,定时,用time.sleep()来定义多长时间后触 ...

  7. 2023-10-21:用go语言,一共有三个服务A、B、C,网络延时分别为a、b、c 并且一定有:1 <= a <= b <= c <= 10^9 但是具体的延时数字丢失了,只有单次调用的时间 一次调

    2023-10-21:用go语言,一共有三个服务A.B.C,网络延时分别为a.b.c 并且一定有:1 <= a <= b <= c <= 10^9 但是具体的延时数字丢失了,只 ...

  8. Rust学习 | Rustlings通关记录与题解

    2023年6月19日决定对rust做一个重新的梳理,整理今年4月份做完的rustlings,根据自己的理解来写一份题解,记录在此. 周折很久,因为中途经历了推免的各种麻烦事,以及选择数据库作为未来研究 ...

  9. Redis 6 学习笔记 2 —— 简单了解订阅和发布(Pub/Sub),JDK17环境下用Jedis 4.3.1连接Redis并模拟验证码发送

    REDIS pubsub -- Redis中国用户组(CRUG) 什么是发布和订阅 Redis发布订阅是一种通信模式:发送者(Pub)发送消息,订阅者(Sub)接收消息.Redis客户端可以订阅任意数 ...

  10. Kubernetes---修改证书可用年限

    kubeadm---修改apiserver证书有效期 源码编译自签证书: 需要有go环境,从github源码仓库拉取k8s对应版本的源码进行修改/编译.覆盖原来的kubeadm即可. 1.查询证书可用 ...