自己动手写 Java 虚拟机笔记 - 第四部分:实现运行时数据区

自己动手写 Java 虚拟机笔记 - 第四部分:实现运行时数据区

Luca Ju
2025-06-16 / 0 评论 / 4 阅读 / 正在检测是否收录...

前言

在前一章中,我们完成了 Class 文件的解析,获取了类的结构信息(字段、方法、常量等)。本章将聚焦 JVM 的运行时数据区—— 这是 JVM 执行字节码时存储数据和中间结果的核心区域,也是实现方法调用、变量存储的基础。

参考资料

《自己动手写 Java 虚拟机》—— 张秀宏

开发环境

工具 / 环境版本说明
操作系统MacOS 15.5基于 Intel/Apple Silicon 均可
JDK1.8用于字节码分析和测试
Go 语言1.23.10项目开发主语言

第四章:运行时数据区核心实现

运行时数据区是 JVM 执行程序时的 “内存空间”,主要包含线程私有区域(程序计数器、虚拟机栈、本地方法栈)和线程共享区域(方法区、堆)。本章重点实现线程私有区域的核心结构,为后续字节码执行打下基础。

一、运行时数据区整体架构

JVM 运行时数据区的结构如图所示,其中线程私有区域与线程一一对应,随线程创建而创建、销毁而销毁;共享区域则被所有线程共享。

ch04rtdata.png

本章核心实现:线程(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)” 中,其中 longdouble 占 2 个槽位,其他类型占 1 个槽位。

2. 引用数据类型

  • 特点:存储对象的引用(指针),而非对象本身。
  • 类型:类实例、数组、接口等。
  • 存储:引用存放在局部变量表或操作数栈的 1 个槽位中,指向堆中的实际对象。

ch04datatype.png

三、核心结构实现

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
}

核心细节

  • longdouble 占 2 个槽位,因此存储时需要占用 indexindex+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--,确保操作安全。
  • 与局部变量表类似,longdouble 占 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
  • 预期输出:局部变量表和操作数栈的读取结果与存储值一致,无异常报错。

ch04test.png

本章小结

本章实现了 JVM 运行时数据区的核心结构,包括:

  1. 线程(Thread):通过程序计数器记录执行位置,通过虚拟机栈管理方法调用。
  2. 虚拟机栈(Stack):通过栈帧的压入 / 弹出模拟方法调用与返回,控制栈深度防止溢出。
  3. 栈帧(Frame):封装局部变量表和操作数栈,是方法执行的基本单位。
  4. 局部变量表(LocalVars):存储方法参数和局部变量,支持基本类型和引用类型的存储。
  5. 操作数栈(OperandStack):存储字节码执行的中间结果,支持基本类型和引用类型的入栈 / 出栈。

这些结构是后续执行字节码指令的基础 —— 下一章将实现指令集与解释器,结合运行时数据区执行具体的指令逻辑。

源码地址:https://github.com/Jucunqi/jvmgo.git
0

评论 (0)

取消