Golang实现JAVA虚拟机-运行时数据区
原文链接:https://gaoyubo.cn/blogs/8ae1f4ca.html
前置
一、运行时数据区概述
JVM学习: JVM-运行时数据区
运行时数据区可以分为两类:一类是多线程共享的,另一类则是线程私有的。
- 多线程共享的运行时数据区需要在Java虚拟机启动时创建好,在Java虚拟机退出时销毁。
- 对象实例存储在
堆区 - 类信息数据存储在
方法区 - 从逻辑上来讲,方法区其实也是堆的一部分。
- 对象实例存储在
- 线程私有的运行时数据区则在创建线程时才创建,线程退出时销毁。
- pc寄存器(Program Counter):执行java方法表示:正在执行的Java虚拟机指令的地址;执行本地方法:pc寄存器无意义
- Java虚拟机栈(JVM Stack)。
- 栈帧(Stack Frame),帧中保存方法执行的状态
- 局部变量表(Local Variable):存放方法参数和方法内定义的局部变量。
- 操作数栈(Operand Stack)等。
- 栈帧(Stack Frame),帧中保存方法执行的状态

虚拟机实现者可以使用任何垃圾回收算 法管理堆,甚至完全不进行垃圾收集也是可以的。
由于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虚拟机-运行时数据区的更多相关文章
- 《深入理解Java虚拟机》(二)Java虚拟机运行时数据区
Java虚拟机运行时数据区 详解 2.1 概述 本文参考的是周志明的 <深入理解Java虚拟机>第二章 ,为了整理思路,简单记录一下,方便后期查阅. 2.2 运行时数据区域 Java虚拟机 ...
- Java虚拟机运行时数据区
运行时数据区程序计数器Java虚拟机栈本地方法栈Java堆(GC堆)方法区运行时常量池 运行时数据区 Java虚拟机在运行Java程序时,会将它所管理的内存划分为若干个内存区域.这些数据区域有各自的用 ...
- 面试常问的 Java 虚拟机运行时数据区
写在前面 本文描述的有关于 JVM 的运行时数据区是基于 HotSpot 虚拟机. 概述 JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以 ...
- Java 虚拟机运行时数据区
写在前面 本文描述的有关于 JVM 的运行时数据区是基于 HotSpot 虚拟机. 概述 JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以 ...
- 【深入理解Java虚拟机】Java虚拟机运行时数据区
Java虚拟机运行时数据区 线程私有 程序计数器 1.当前线程所执行的字节码的行号指示器. 2.唯一不会发生OutOfMemoryError的区域 3.如果执行的是java方法,计数器值为虚拟机字节码 ...
- 笔记:Java虚拟机运行时数据区
Java虚拟机在执行Java程序的过程中会把它管的内存划分为以下若干个不同的区域: 1.程序计数器 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器:由于Java虚拟机的 ...
- 【JVM从小白学成大佬】2.Java虚拟机运行时数据区
目录 1.运行时数据区介绍 2.堆(Heap) 是否可能有两个对象共用一段内存的事故? 3.方法区(Method Area) 4.程序计数器(Program Counter Register) 5.虚 ...
- 【JVM学习】2.Java虚拟机运行时数据区
来源: 公众号: 猿人谷 这里我们先说句题外话,相信大家在面试中经常被问到介绍Java内存模型,我在面试别人时也会经常问这个问题.但是,往往都会令我比较尴尬,我还话音未落,面试者就会"背诵& ...
- Java 虚拟机运行时数据区详解
本文摘自深入理解 Java 虚拟机第三版 概述 Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟 ...
- 深入理解Java虚拟机&运行时数据区
其中,程序计数器.虚拟机栈.本地方法栈3个区域随线程而生,随线程而灭.
随机推荐
- 5.0 CRC32校验技术概述
CRC校验技术是用于检测数据传输或存储过程中是否出现了错误的一种方法,校验算法可以通过计算应用与数据的循环冗余校验(CRC)检验值来检测任何数据损坏.通过运用本校验技术我们可以实现对特定内存区域以及磁 ...
- Django框架——路由控制、视图层
文章目录 1 路由控制 一 Django中路由的作用 二 简单的路由配置 三 有名分组 四 路由分发 五 反向解析 六 名称空间 七 django2.0版的path 基本示例 path转化器 注册自定 ...
- PowerShell 多平台一键生成 Blu-ray Live 分轨
前言 本人 n 年前的需求,需要自动化的将 Blu-ray Live 转换成 FLAC 格式的文件(自听&发种). ️ 注意:本脚本仅支持输出 flac ! 前提 计算机安装有 PowerSh ...
- 创建vue项目并搭建JSONSERVER
1.该前提是你已经搭建好vue-cli脚手架,开始创建一个新项目,输入 vue init webpack demo(demo是自定义项目名). 2.cd demo 进入项目安装依赖 3.在已经创建的项 ...
- 【分段传输】c#使用IAsyncEnumerable实现流式分段传输
引言 在使用SSE的时候,前端可以实现流式传输,但是有个问题就是这是一个独占的连接,相当于如果你不手动关闭连接,就会一直请求,一直连接调用接口,而且发送的数据格式也是按照定义好的协议来,而使用c#自带 ...
- 记一个 Android 14 适配引发的Android 存储权限问题
一.bug 背景 项目中有下面这样一段代码,在 Android T 版本运行正常,现在适配到 Android U 上之后,运行时 crash 了.... ... values.put(MediaSto ...
- Python 数据库应用教程:安装 MySQL 及使用 MySQL Connector
Python可以用于数据库应用程序. 其中最流行的数据库之一是MySQL. MySQL数据库 为了能够在本教程中尝试代码示例,您应该在计算机上安装MySQL. 您可以在 MySQL官方网站 下载MyS ...
- 文心一言 VS 讯飞星火 VS chatgpt (140)-- 算法导论11.4 5题
五.用go语言,考虑一个装载因子为a的开放寻址散列表.找出一个非零的a值,使得一次不成功查找的探查期望数是一次成功查找的探查期望数的 2 倍.这两个探查期望数可以使用定理11.6 和定理 11.8 中 ...
- 关于一类最优解存在长度为 $k$ 的循环节的问题
灵感来源 问题形式:给定长度为 \(n\) 的序列,要求选出一些位置,使这些位置满足限制条件 \(T\),其中 \(T\) 可以表述为一个长度为 \(k\) 的环满足条件 \(T'\),选出第 \(i ...
- 发现AI自我意识:进入混合增强只能的纪元
执行性思维:人工智能的现实优势 如何解构人类的思维模型是一个跨多学科的综合性问题.本文仅针对AI领域发展方向预测以及理解,提出一个简化的模型.我认为人类的思维基于思考的目的性可以分为:执行性思维和创造 ...