前言
在前一章中,我们完成了 Class 文件的解析,获取了类的结构信息(字段、方法、常量等)。本章将聚焦 JVM 的运行时数据区—— 这是 JVM 执行字节码时存储数据和中间结果的核心区域,也是实现方法调用、变量存储的基础。
参考资料
《自己动手写 Java 虚拟机》—— 张秀宏
开发环境
| 工具 / 环境 | 版本 | 说明 |
|---|---|---|
| 操作系统 | MacOS 15.5 | 基于 Intel/Apple Silicon 均可 |
| JDK | 1.8 | 用于字节码分析和测试 |
| Go 语言 | 1.23.10 | 项目开发主语言 |
第四章:运行时数据区核心实现
运行时数据区是 JVM 执行程序时的 “内存空间”,主要包含线程私有区域(程序计数器、虚拟机栈、本地方法栈)和线程共享区域(方法区、堆)。本章重点实现线程私有区域的核心结构,为后续字节码执行打下基础。
一、运行时数据区整体架构
JVM 运行时数据区的结构如图所示,其中线程私有区域与线程一一对应,随线程创建而创建、销毁而销毁;共享区域则被所有线程共享。

本章核心实现:线程(Thread)、虚拟机栈(Stack)、栈帧(Frame)、局部变量表(LocalVars)、操作数栈(OperandStack)。
二、数据类型在运行时的存储方式
JVM 中的数据类型分为两类,其存储方式直接影响运行时数据区的设计:
1. 基本数据类型
- 特点:存储数据本身,不需要引用。
- 类型:
int(4 字节)、long(8 字节)、float(4 字节)、double(8 字节)、byte/short/char(均按 int 存储)、boolean(按 int 存储,0 为 false,非 0 为 true)。 - 存储:基本类型直接存放在局部变量表或操作数栈的 “槽位(Slot)” 中,其中
long和double占 2 个槽位,其他类型占 1 个槽位。
2. 引用数据类型
- 特点:存储对象的引用(指针),而非对象本身。
- 类型:类实例、数组、接口等。
- 存储:引用存放在局部变量表或操作数栈的 1 个槽位中,指向堆中的实际对象。

三、核心结构实现
1. 线程(Thread):运行时数据区的 “载体”
线程是 JVM 执行的基本单位,每个线程对应一个虚拟机栈。线程还包含程序计数器(PC),用于记录当前执行的字节码指令地址。
package rtda
// Thread 封装线程相关的运行时数据
type Thread struct {
pc int // 程序计数器:记录当前执行的字节码指令地址
stack *Stack // 虚拟机栈:存储方法调用的栈帧
}
// NewThread 创建新线程,初始化虚拟机栈(默认最大深度 1024)
func NewThread() *Thread {
return &Thread{stack: newStack(1024)}
}
// PC 获取当前程序计数器的值
func (t *Thread) PC() int {
return t.pc
}
// SetPC 更新程序计数器的值
func (t *Thread) SetPC(pc int) {
t.pc = pc
}
// PushFrame 向虚拟机栈中压入栈帧
func (t *Thread) PushFrame(frame *Frame) {
t.stack.push(frame)
}
// PopFrame 从虚拟机栈中弹出栈帧
func (t *Thread) PopFrame() *Frame {
return t.stack.pop()
}
// CurrentFrame 获取当前正在执行的栈帧(栈顶帧)
func (t *Thread) CurrentFrame() *Frame {
return t.stack.top()
}核心作用:线程是串联所有运行时结构的载体,通过 PC 记录执行位置,通过栈管理方法调用链路。
2. 虚拟机栈(Stack):管理方法调用的 “栈结构”
虚拟机栈由多个栈帧(Frame)组成,遵循 “先进后出” 原则,用于存储方法调用的状态(如局部变量、操作数等)。
package rtda
// Stack 虚拟机栈:存储栈帧的链表结构
type Stack struct {
maxSize uint // 栈的最大深度(防止栈溢出)
size uint // 当前栈深度
_top *Frame // 栈顶帧(当前执行的方法帧)
}
// push 向栈中压入栈帧,若栈满则抛出 StackOverflowError
func (s *Stack) push(frame *Frame) {
if s.size >= s.maxSize {
panic("java.lang.StackOverflowError") // 模拟 JVM 栈溢出异常
}
// 维护栈帧链表关系(新帧的 lower 指向原栈顶)
if s._top != nil {
frame.lower = s._top
}
s._top = frame // 更新栈顶为新帧
s.size++ // 栈深度+1
}
// pop 从栈中弹出栈帧,若栈空则抛出异常
func (s *Stack) pop() *Frame {
if s._top == nil {
panic("jvm stack is empty") // 栈空异常
}
top := s._top // 记录当前栈顶帧
s._top = top.lower // 更新栈顶为下一个帧
s.size-- // 栈深度-1
return top
}
// top 获取当前栈顶帧(不弹出)
func (s *Stack) top() *Frame {
if s._top == nil {
panic("jvm stack is empty!")
}
return s._top
}
// newStack 创建指定最大深度的虚拟机栈
func newStack(size uint) *Stack {
return &Stack{maxSize: size}
}核心作用:通过栈帧的压入 / 弹出模拟方法的调用与返回,maxSize 控制栈深度防止溢出(如递归调用过深时触发 StackOverflowError)。
3. 栈帧(Frame):方法执行的 “状态容器”
栈帧是方法执行的基本单位,每个方法调用对应一个栈帧,包含局部变量表和操作数栈。
package rtda
// Frame 栈帧:存储方法执行的局部变量和操作数
type Frame struct {
lower *Frame // 下一个栈帧(当前方法的调用者帧)
localVars LocalVars // 局部变量表:存储方法的参数和局部变量
operandStack *OperandStack // 操作数栈:存储字节码执行的中间结果
}
// NewFrame 创建栈帧,需要指定局部变量表大小和操作数栈大小
// (大小从 Class 文件的方法 Code 属性中获取)
func NewFrame(maxLocals, maxStack uint) *Frame {
return &Frame{
localVars: newLocalVars(maxLocals), // 初始化局部变量表
operandStack: newOperandStack(maxStack), // 初始化操作数栈
}
}
// LocalVars 获取局部变量表
func (f *Frame) LocalVars() LocalVars {
return f.localVars
}
// OperandStack 获取操作数栈
func (f *Frame) OperandStack() *OperandStack {
return f.operandStack
}核心作用:栈帧是方法执行的 “快照”,localVars 存储方法的输入(参数和局部变量),operandStack 存储执行过程中的中间结果,两者配合完成字节码指令的计算。
4. 局部变量表(LocalVars):存储方法参数和局部变量
局部变量表是一个定长数组(槽位集合),用于存储方法的参数和局部变量,索引从 0 开始。
package rtda
import "math"
// Slot 局部变量表和操作数栈的基本存储单元
type Slot struct {
num int32 // 存储基本类型数据(int、float 等)
ref *Object // 存储引用类型数据(对象指针)
}
// LocalVars 局部变量表:由 Slot 数组组成
type LocalVars []Slot
// newLocalVars 创建指定大小的局部变量表
func newLocalVars(maxSize uint) LocalVars {
if maxSize > 0 {
return make([]Slot, maxSize)
}
return nil
}
// 基本类型存储与读取(int)
func (l LocalVars) SetInt(index uint, val int32) {
l[index].num = val // int 直接存放在 num 中
}
func (l LocalVars) GetInt(index uint) int32 {
return l[index].num
}
// 基本类型存储与读取(float)
func (l LocalVars) SetFloat(index uint, val float32) {
bits := math.Float32bits(val) // float 转 uint32 存储
l[index].num = int32(bits)
}
func (l LocalVars) GetFloat(index uint) float32 {
bits := uint32(l[index].num) // 取出 uint32 转 float
return math.Float32frombits(bits)
}
// 基本类型存储与读取(long,占 2 个槽位)
func (l LocalVars) SetLong(index uint, val int64) {
// 低 32 位存 index,高 32 位存 index+1
l[index].num = int32(val)
l[index+1].num = int32(val >> 32)
}
func (l LocalVars) GetLong(index uint) int64 {
low := uint32(l[index].num) // 取出低 32 位
high := uint32(l[index+1].num) // 取出高 32 位
return int64(high)<<32 | int64(low) // 合并为 64 位 long
}
// 基本类型存储与读取(double,占 2 个槽位)
func (l LocalVars) SetDouble(index uint, val float64) {
bits := math.Float64bits(val) // double 转 uint64 存储
l.SetLong(index, int64(bits)) // 复用 long 的存储逻辑
}
func (l LocalVars) GetDouble(index uint) float64 {
long := l.GetLong(index) // 复用 long 的读取逻辑
bits := uint64(long)
return math.Float64frombits(bits) // 转 float64
}
// 引用类型存储与读取
func (l LocalVars) SetRef(index uint, val *Object) {
l[index].ref = val // 引用存放在 ref 中
}
func (l LocalVars) GetRef(index uint) *Object {
return l[index].ref
}核心细节:
long和double占 2 个槽位,因此存储时需要占用index和index+1,读取时也需从两个槽位合并数据。- 引用类型通过
ref字段存储对象指针,指向堆中的实际对象(本章暂不实现堆,用*Object占位)。
5. 操作数栈(OperandStack):存储字节码执行的中间结果
操作数栈是一个动态数组,用于存储字节码指令的操作数和计算结果,遵循 “先进后出” 原则。
package rtda
import "math"
// OperandStack 操作数栈:由 Slot 数组组成,支持 push/pop 操作
type OperandStack struct {
size uint // 当前栈深度
slots []Slot // 存储操作数的 Slot 数组
}
// newOperandStack 创建指定大小的操作数栈
func newOperandStack(stackSize uint) *OperandStack {
if stackSize > 0 {
return &OperandStack{slots: make([]Slot, stackSize)}
}
return nil
}
// 基本类型入栈/出栈(int)
func (o *OperandStack) PushInt(val int32) {
o.slots[o.size].num = val
o.size++ // 栈深度+1
}
func (o *OperandStack) PopInt() int32 {
o.size-- // 栈深度-1
return o.slots[o.size].num
}
// 基本类型入栈/出栈(float)
func (o *OperandStack) PushFloat(val float32) {
bits := math.Float32bits(val)
o.slots[o.size].num = int32(bits)
o.size++
}
func (o *OperandStack) PopFloat() float32 {
o.size--
bits := uint32(o.slots[o.size].num)
return math.Float32frombits(bits)
}
// 基本类型入栈/出栈(long,占 2 个槽位)
func (o *OperandStack) PushLong(val int64) {
low := int32(val) // 低 32 位
o.slots[o.size].num = low
o.size++
high := int32(val >> 32) // 高 32 位
o.slots[o.size].num = high
o.size++ // 栈深度+2
}
func (o *OperandStack) PopLong() int64 {
o.size -= 2 // 栈深度-2
low := uint32(o.slots[o.size].num)
high := uint32(o.slots[o.size+1].num)
return int64(high)<<32 | int64(low)
}
// 基本类型入栈/出栈(double,占 2 个槽位)
func (o *OperandStack) PushDouble(val float64) {
bits := math.Float64bits(val)
o.PushLong(int64(bits)) // 复用 long 的入栈逻辑
}
func (o *OperandStack) PopDouble() float64 {
bits := uint64(o.PopLong()) // 复用 long 的出栈逻辑
return math.Float64frombits(bits)
}
// 引用类型入栈/出栈
func (o *OperandStack) PushRef(val *Object) {
o.slots[o.size].ref = val
o.size++
}
func (o *OperandStack) PopRef() *Object {
o.size--
ref := o.slots[o.size].ref
o.slots[o.size].ref = nil // 弹出后清空引用(帮助 GC)
return ref
}核心细节:
- 操作数栈的
size字段记录当前栈深度,入栈时size++,出栈时size--,确保操作安全。 - 与局部变量表类似,
long和double占 2 个槽位,入栈时栈深度 + 2,出栈时 - 2。
四、测试运行时数据区功能
为验证局部变量表和操作数栈的正确性,我们编写测试代码,模拟基本类型和引用类型的存储与读取。
1. 测试代码实现
// 修改 startJVM 函数,添加测试逻辑
func startJVM(cmd *Cmd) {
// 创建栈帧(局部变量表大小 100,操作数栈大小 100)
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.PopInt())
println(ops.PopInt())
println(ops.PopLong())
println(ops.PopLong())
println(ops.PopFloat())
println(ops.PopDouble())
println(ops.PopRef())
}2. 执行测试与结果验证
- 编译命令:
go install ./ch04/ - 执行命令:
ch04 - 预期输出:局部变量表和操作数栈的读取结果与存储值一致,无异常报错。

本章小结
本章实现了 JVM 运行时数据区的核心结构,包括:
- 线程(Thread):通过程序计数器记录执行位置,通过虚拟机栈管理方法调用。
- 虚拟机栈(Stack):通过栈帧的压入 / 弹出模拟方法调用与返回,控制栈深度防止溢出。
- 栈帧(Frame):封装局部变量表和操作数栈,是方法执行的基本单位。
- 局部变量表(LocalVars):存储方法参数和局部变量,支持基本类型和引用类型的存储。
- 操作数栈(OperandStack):存储字节码执行的中间结果,支持基本类型和引用类型的入栈 / 出栈。
这些结构是后续执行字节码指令的基础 —— 下一章将实现指令集与解释器,结合运行时数据区执行具体的指令逻辑。
源码地址:https://github.com/Jucunqi/jvmgo.git
评论 (0)