首页
关于这个博客
Search
1
Java 实现Google 账号单点登录(OAuth 2.0)全流程解析
231 阅读
2
Spring AI 无法获取大模型深度思考内容?解决方案来了
202 阅读
3
微信小程序实现页面返回前确认弹窗:兼容左上角返回与右滑返回
91 阅读
4
服务器遭遇 XMRig 挖矿程序入侵排查与清理全记录
66 阅读
5
解决 Mac 版 PicGo 无法打开问题:“已损坏,无法打开” 报错处理指南
37 阅读
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
登录
Search
标签搜索
java虚拟机
JVM
保姆级教程
Java
Spring AI
SpringBoot
Nginx
WebFlux
Spring
cdn
https
dcdn
网站加速
Tool
图片导出
服务部署
源码解析
单点登录
google
sso
Luca Ju
累计撰写
35
篇文章
累计收到
1
条评论
首页
栏目
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
页面
关于这个博客
搜索到
18
篇与
的结果
2025-06-30
自己动手写 Java 虚拟机笔记 - 第七部分:方法调用与返回机制
前言在前一章中,我们实现了类与对象的核心机制,包括类信息存储、对象创建和字段访问等。本章将聚焦 JVM 的方法调用与返回机制—— 这是实现函数执行、参数传递和结果返回的核心逻辑,涉及方法符号引用解析、调用指令执行、栈帧管理和类初始化等关键流程,是 JVM 支持面向对象编程的重要基础。参考资料《自己动手写 Java 虚拟机》—— 张秀宏开发环境工具 / 环境版本说明操作系统MacOS 15.5基于 Intel/Apple Silicon 均可JDK1.8用于字节码分析和测试Go 语言1.23.10项目开发主语言第七章:方法调用与返回机制实现方法调用是程序执行的核心动作,JVM 通过特定指令(如 invoke_static、invoke_virtual 等)实现不同类型方法的调用,并通过返回指令(如 ireturn、lreturn 等)完成结果传递。本章将详细实现这些机制,包括符号引用解析、参数传递、栈帧管理和类初始化触发等逻辑。一、方法调用概述JVM 中的方法按调用方式可分为静态方法、实例方法、抽象方法等,不同类型的方法通过不同的指令调用。理解方法调用的核心概念和指令分类是实现的基础。1. 方法类型与调用指令JVM 提供的方法调用指令及其适用场景如下:指令名称适用场景特点invoke_static调用静态方法编译期确定目标方法,直接通过类名调用invoke_special调用构造器、super 方法、private 方法编译期确定目标方法,不涉及动态绑定invoke_virtual调用非静态方法(除 private 和构造器外)运行期通过对象实际类型查找方法(动态绑定,支持多态)invoke_interface调用接口方法运行期查找实现接口的具体方法,比 invoke_virtual 多一步接口适配invoke_dynamic动态调用(如 Lambda 表达式、方法引用)JDK 8 新增,支持动态语言特性(本章暂不实现)核心区别:静态方法和 private 方法通过 “静态绑定” 在编译期确定目标方法;实例方法通过 “动态绑定” 在运行期根据对象实际类型查找方法,这是多态特性的核心实现。2. 方法调用的核心流程无论哪种调用指令,方法调用的基本流程一致,可概括为:解析常量池中的方法符号引用,获取目标方法的直接引用;从操作数栈弹出方法参数(按调用约定顺序);创建目标方法的栈帧并压入虚拟机栈;将弹出的参数放入新栈帧的局部变量表(参数传递);执行目标方法的字节码指令;方法执行完毕后,通过返回指令弹出当前栈帧,将返回值推入调用方栈帧的操作数栈。二、方法符号引用解析方法调用的第一步是将常量池中的 “方法符号引用”(编译期的间接引用)解析为 “直接引用”(运行期的内存地址),这是后续调用的基础。1. 非接口方法的解析流程以 MethodRef(类方法符号引用)为例,解析逻辑如下:// ResolveMethod 将方法符号引用解析为直接引用(Method 实例) func (r *MethodRef) ResolveMethod() *Method { if r.method == nil { r.resolveMethodRef() // 未解析则执行解析逻辑 } return r.method } // resolveMethodRef 实际执行方法解析 func (r *MethodRef) resolveMethodRef() { currentClass := r.cp.class // 引用所在的当前类 methodClass := r.ResolveClass() // 解析方法所属的类(符号引用→直接引用) // 校验:方法所属类不能是接口(接口方法需用 InterfaceMethodref) if methodClass.IsInterface() { panic("java.lang.IncompatibleClassChangeError") } // 查找目标方法(在类、父类、接口中递归匹配) method := lookupMethod(methodClass, r.name, r.descriptor) if method == nil { panic("java.lang.NoSuchMethodError") // 方法未找到 } // 校验访问权限(当前类是否有权限调用目标方法) if !method.isAccessibleTo(currentClass) { panic("java.lang.IllegalAccessError") } r.method = method // 保存解析结果(直接引用) }2. 方法查找逻辑方法查找需在类本身、父类及实现的接口中递归匹配,确保找到正确的方法:// lookupMethod 查找类或接口中的方法 func lookupMethod(class *Class, name string, descriptor string) *Method { // 1. 在当前类中查找 method := LookupMethodInClass(class, name, descriptor) if method != nil { return method } // 2. 在接口中查找(递归检查所有实现的接口) return lookupMethodInInterface(class.interfaces, name, descriptor) } // LookupMethodInClass 在单个类中查找方法(按名称和描述符匹配) func LookupMethodInClass(class *Class, name string, descriptor string) *Method { // 遍历类的所有方法,匹配名称和描述符 for _, method := range class.methods { if method.name == name && method.descriptor == descriptor { return method } } // 类中未找到,递归查找父类 if class.superClass != nil { return LookupMethodInClass(class.superClass, name, descriptor) } return nil } // lookupMethodInInterface 在接口中递归查找方法 func lookupMethodInInterface(ifaces []*Class, name string, descriptor string) *Method { for _, iface := range ifaces { // 遍历接口的所有方法 for _, method := range iface.methods { if method.name == name && method.descriptor == descriptor { return method } } // 接口未找到,递归查找父接口 method := lookupMethodInInterface(iface.interfaces, name, descriptor) if method != nil { return method } } return nil }关键逻辑:方法查找遵循 “类优先于接口”“子类优先于父类” 的原则,确保符合 Java 的方法继承和重写规则。三、方法调用与参数传递解析到目标方法后,需创建栈帧、传递参数并执行方法,这是方法调用的核心执行阶段。1. 方法调用的核心实现// InvokeMethod 执行方法调用的核心逻辑 func InvokeMethod(invokerFrame *rtda.Frame, method *heap.Method) { thread := invokerFrame.Thread() // 获取当前线程 // 1. 创建目标方法的栈帧(根据方法的 maxLocals 和 maxStack 初始化) newFrame := thread.NewFrame(method) // 2. 将新栈帧压入虚拟机栈 thread.PushFrame(newFrame) // 3. 参数传递:从调用方操作数栈弹出参数,放入新栈帧的局部变量表 argSlotCount := int(method.ArgSlotCount()) // 获取参数占用的槽位总数 if argSlotCount > 0 { // 从后往前弹出参数(栈是先进后出,参数顺序需保持一致) for i := argSlotCount - 1; i >= 0; i-- { slot := invokerFrame.OperandStack().PopSlot() // 弹出参数 newFrame.LocalVars().SetSlot(uint(i), slot) // 存入局部变量表 } } // 特殊处理:跳过 Native 方法(本章暂不实现 Native 方法逻辑) if method.IsNative() { if method.Name() == "registerNatives" { // 忽略 Object 类的 registerNatives 方法(无实际逻辑) thread.PopFrame() } else { panic(fmt.Sprintf("未实现 Native 方法:%v.%v%v", method.Class().Name(), method.Name(), method.Descriptor())) } } }参数传递细节:方法的参数数量和类型通过描述符(如 (ILjava/lang/String;)V 表示 2 个参数)确定,ArgSlotCount() 计算参数占用的槽位总数(long 和 double 占 2 个槽位);由于操作数栈是 “先进后出” 结构,参数需从后往前弹出,才能按正确顺序存入局部变量表(索引 0 对应第一个参数)。四、返回指令:结果传递与栈帧管理方法执行完毕后,通过返回指令将结果传递给调用方,并弹出当前栈帧,恢复调用方的执行。1. 返回指令的实现(以 ireturn 为例)ireturn 用于返回 int 类型结果,其他类型(如 long、float)的返回指令逻辑类似:// IRETURN 返回 int 类型结果 type IRETURN struct { base.NoOperandsInstruction } func (i *IRETURN) Execute(frame *rtda.Frame) { thread := frame.Thread() // 1. 弹出当前方法的栈帧 currentFrame := thread.PopFrame() // 2. 从当前栈帧的操作数栈弹出返回值 result := currentFrame.OperandStack().PopInt() // 3. 将返回值推入调用方栈帧的操作数栈 invokerFrame := thread.TopFrame() invokerFrame.OperandStack().PushInt(result) }2. 不同类型返回指令的共性所有返回指令的核心流程一致,差异仅在于返回值的类型处理:lreturn:返回 long 类型,弹出 8 字节值并推入调用方栈;freturn/dreturn:返回 float/double 类型,通过浮点转码处理;areturn:返回引用类型,弹出对象引用并推入调用方栈;return:无返回值(void),仅弹出当前栈帧。五、核心方法调用指令实现不同的方法调用指令对应不同的解析和执行逻辑,以下以 invoke_virtual(动态绑定核心指令)为例说明。1. invoke_virtual 指令:支持多态的实例方法调用// INVOKE_VIRTUAL 调用实例方法(动态绑定) type INVOKE_VIRTUAL struct { base.Index16Instruction // 包含常量池索引(指向方法符号引用) } func (i *INVOKE_VIRTUAL) Execute(frame *rtda.Frame) { currentClass := frame.Method().Class() cp := currentClass.ConstantPool() // 1. 解析方法符号引用 methodRef := cp.GetConstant(i.Index).(*heap.MethodRef) resolvedMethod := methodRef.ResolveMethod() // 校验:不能调用静态方法 if resolvedMethod.IsStatic() { panic("java.lang.IncompatibleClassChangeError") } // 2. 获取操作数栈中的对象引用(this) // 从栈顶弹出参数后,剩余的第一个元素为 this 引用 ref := frame.OperandStack().GetRefFromTop(resolvedMethod.ArgSlotCount() - 1) if ref == nil { // 特殊处理:支持 System.out.println 等常用方法(简化实现) if methodRef.Name() == "println" { _println(frame.OperandStack(), methodRef.Descriptor()) return } panic("java.lang.NullPointerException") // 对象为 null 时调用方法 } // 3. 权限校验(protected 方法的访问控制) if resolvedMethod.IsProtected() && resolvedMethod.Class().IsSuperClassOf(currentClass) && resolvedMethod.Class().GetPackageName() != currentClass.GetPackageName() && ref.Class() != currentClass && !ref.Class().IsSubClassOf(currentClass) { panic("java.lang.IllegalAccessError") } // 4. 动态绑定:根据对象实际类型查找方法(而非编译期类型) methodToBeInvoked := heap.LookupMethodInClass(ref.Class(), methodRef.Name(), methodRef.Descriptor()) if methodToBeInvoked == nil || methodToBeInvoked.IsAbstract() { panic("java.lang.AbstractMethodError") // 方法未实现 } // 5. 执行方法调用 base.InvokeMethod(frame, methodToBeInvoked) }动态绑定核心:invoke_virtual 不直接使用符号引用解析的方法,而是根据对象的实际类型(ref.Class())重新查找方法,这确保了运行时调用的是子类重写的方法,实现多态特性。六、改进解释器:支持日志与多方法执行为便于调试和跟踪方法调用流程,改进解释器以支持指令日志输出,并完善多方法连续执行的逻辑。1. 解释器主循环优化// loop 解释器主循环,支持指令日志输出 func loop(thread *rtda.Thread, logInst bool) { reader := &base.BytecodeReader{} for { frame := thread.CurrentFrame() // 获取当前栈顶栈帧 pc := frame.NextPC() // 获取程序计数器 thread.SetPC(pc) // 解析指令 reader.Reset(frame.Method().Code(), pc) opcode := reader.ReadInt8() // 读取 opcode inst := instructions.NewInstruction(byte(opcode)) // 创建指令实例 inst.FetchOperands(reader) // 读取操作数 // 输出指令日志(若启用) if logInst { logInstruction(frame, inst) } // 执行指令并更新程序计数器 frame.SetNextPC(reader.PC()) inst.Execute(frame) // 线程栈为空时结束循环(所有方法执行完毕) if thread.IsStackEmpty() { break } } }日志功能:通过 verbose:inst 参数启用指令日志,可输出当前执行的指令、PC 地址等信息,便于跟踪方法调用流程和调试问题。七、类初始化:触发与执行 <clinit> 方法类在首次被使用时需执行初始化(执行类构造器 <clinit> 方法),方法调用是触发初始化的重要场景之一。1. 类初始化的触发条件以下情况会触发类初始化(执行 <clinit> 方法):执行 new 指令创建对象时,类未初始化;执行 putstatic/getstatic 指令访问静态字段时,类未初始化;执行 invoke_static 指令调用静态方法时,类未初始化;初始化子类时,父类未初始化;反射操作访问类时,类未初始化。2. 类初始化逻辑实现// InitClass 执行类初始化(触发 <clinit> 方法) func InitClass(thread *rtda.Thread, class *heap.Class) { class.StartInit() // 标记类开始初始化(防止重复初始化) scheduleClinit(thread, class) // 执行类构造器 <clinit> 方法 initSuperClass(thread, class) // 递归初始化父类 } // scheduleClinit 计划执行 <clinit> 方法 func scheduleClinit(thread *rtda.Thread, class *heap.Class) { clinit := class.GetClinitMethod() // 获取 <clinit> 方法 if clinit != nil { // 创建 <clinit> 方法的栈帧并压入栈 frame := thread.NewFrame(clinit) thread.PushFrame(frame) } } // initSuperClass 初始化父类(确保父类先于子类初始化) func initSuperClass(thread *rtda.Thread, class *heap.Class) { if !class.IsInterface() { superClass := class.SuperClass() if superClass != nil && !superClass.InitStarted() { InitClass(thread, superClass) // 递归初始化父类 } } }核心逻辑:类初始化通过执行 <clinit> 方法完成,该方法由编译器自动生成,包含静态变量初始化和静态代码块逻辑。初始化过程中会确保父类先于子类初始化,符合 Java 的类加载规范。本章小结本章实现了 JVM 方法调用与返回的核心机制,重点包括:方法调用指令分类:区分 invoke_static、invoke_virtual 等指令的适用场景,支持静态绑定和动态绑定;符号引用解析:通过递归查找类、父类和接口,将方法符号引用解析为直接引用;参数传递与栈帧管理:创建栈帧并通过操作数栈传递参数,确保方法调用的上下文正确;返回指令实现:通过不同类型的返回指令传递结果,弹出当前栈帧恢复调用方执行;类初始化触发:在方法调用等场景中触发类初始化,执行 <clinit> 方法完成静态变量初始化。这些机制共同支撑了 JVM 对面向对象特性(如多态、继承)的支持,是实现完整 Java 程序执行的关键基础。下一章将进一步完善异常处理和数组支持,增强 JVM 的功能完整性。源码地址:https://github.com/Jucunqi/jvmgo.git
2025年06月30日
4 阅读
0 评论
1 点赞
2025-06-20
自己动手写 Java 虚拟机笔记 - 第六部分:类与对象的核心实现
前言在前一章中,我们实现了指令集和基础解释器,让 JVM 能够执行简单的字节码逻辑。本章将聚焦 JVM 中类与对象的核心机制—— 包括类信息的存储(方法区)、类加载过程、对象创建与访问,以及符号引用解析等关键逻辑,这是 JVM 实现 “面向对象” 特性的基础。参考资料《自己动手写 Java 虚拟机》—— 张秀宏开发环境工具 / 环境版本说明操作系统MacOS 15.5基于 Intel/Apple Silicon 均可JDK1.8用于字节码分析和测试Go 语言1.23.10项目开发主语言第六章:类与对象的核心实现类和对象是 Java 面向对象编程的核心,JVM 需要通过特定的数据结构存储类信息、管理对象生命周期,并提供指令支持类与对象的操作(如创建对象、访问字段等)。本章将实现这些核心机制。一、方法区:类信息的 “仓库”方法区是 JVM 中线程共享的内存区域,用于存储已加载类的元信息(类结构、字段、方法、常量等),是类加载后的数据载体。1. 类信息的核心结构(Class 结构体)Class 结构体整合了从 Class 文件解析的所有类信息,是方法区存储的核心单元:// Class 存储类的完整信息(对应方法区中的类元数据) type Class struct { accessFlags uint16 // 类访问标志(如 public、final、abstract) name string // 类的全限定名(如 "java/lang/String") superClassName string // 父类全限定名(如 "java/lang/Object") interfaceNames []string // 实现的接口全限定名列表 constantPool *ConstantPool // 运行时常量池(类关联的常量) fields []*Field // 类的字段列表 methods []*Method // 类的方法列表 loader *ClassLoader // 加载该类的类加载器 superClass *Class // 父类的 Class 实例(运行时关联) interfaces []*Class // 实现的接口 Class 实例列表(运行时关联) instanceSlotCount uint // 实例字段占用的槽位总数(用于对象内存分配) staticSlotCount uint // 静态字段占用的槽位总数(用于方法区存储) staticVars Slots // 静态变量(存储静态字段的实际值) } // newClass 从 Class 文件解析结果创建 Class 实例 func newClass(cf *classfile.ClassFile) *Class { class := &Class{} class.accessFlags = cf.AccessFlags() class.name = cf.ClassName() class.superClassName = cf.SuperClassName() class.interfaceNames = cf.InterfaceNames() // 初始化常量池、字段、方法 class.constantPool = newConstantPool(class, cf.ConstantPool()) class.fields = newFields(class, cf.Fields()) class.methods = newMethod(class, cf.Methods()) return class }核心字段说明:constantPool:存储类的运行时常量池(字面量和符号引用);fields/methods:存储类的字段和方法信息(从 Class 文件的字段表、方法表解析);staticVars:存储静态字段的实际值(静态变量属于类,而非对象,因此存放在方法区)。2. 字段与方法的结构字段(Field)和方法(Method)是类的核心成员,通过复用 ClassMember 结构体统一管理公共属性:// ClassMember 字段和方法的公共属性 type ClassMember struct { accessFlags uint16 // 成员访问标志(如 public、private、static) name string // 成员名称(如字段名 "age"、方法名 "toString") descriptor string // 成员描述符(如字段 "I" 表示 int,方法 "(I)V" 表示入参 int、返回 void) class *Class // 所属的类 } // Field 类的字段信息 type Field struct { ClassMember // 继承公共属性 constValueIndex uint // 常量值索引(若字段被 final 修饰,指向常量池中的值) slotId uint // 字段在槽位中的索引(实例字段在对象中,静态字段在方法区) } // Method 类的方法信息 type Method struct { ClassMember // 继承公共属性 maxStack uint // 操作数栈最大深度(从 Code 属性获取) maxLocals uint // 局部变量表大小(从 Code 属性获取) code []byte // 方法的字节码指令(从 Code 属性获取) }关键设计:字段和方法通过 slotId 定位存储位置(实例字段的 slotId 用于对象的槽位索引,静态字段的 slotId 用于 staticVars 索引);方法的 maxStack 和 code 等信息来自 Class 文件的 Code 属性,是解释器执行的基础。二、运行时常量池:常量与符号引用的 “容器”运行时常量池是方法区的一部分,存储类加载时从常量池解析的字面量(如整数、字符串)和符号引用(如类、字段、方法的引用),是类与其他结构关联的桥梁。1. 常量池结构// Constant 常量接口(所有常量类型都需实现) type Constant interface{} // ConstantPool 运行时常量池 type ConstantPool struct { class *Class // 所属的类 consts []Constant // 常量数组(存储字面量和符号引用) }2. 符号引用的核心类型符号引用是编译期的概念,用于指代尚未加载的类、字段或方法,在运行时通过解析转化为直接引用(内存地址)。核心符号引用类型如下:符号引用类型作用核心结构ClassRef类的符号引用继承 SymRef,包含类名和所属常量池FieldRef字段的符号引用继承 MemberRef,包含字段名、描述符及所属类引用MethodRef类方法的符号引用继承 MemberRef,包含方法名、描述符及所属类引用InterfaceMethodref接口方法的符号引用继承 MemberRef,逻辑与 MethodRef 类似示例:类符号引用与成员符号引用// SymRef 所有符号引用的基类 type SymRef struct { cp *ConstantPool // 所属常量池 className string // 目标类名(如 "java/lang/Object") class *Class // 解析后的目标类(直接引用,运行时赋值) } // MemberRef 字段和方法符号引用的基类 type MemberRef struct { SymRef // 继承类符号引用的属性 name string // 成员名称(如字段名 "name"、方法名 "getAge") descriptor string // 成员描述符(如字段 "Ljava/lang/String;"、方法 "()I") } // ClassRef 类符号引用 type ClassRef struct { SymRef // 仅需继承类符号引用的基础属性 } // FieldRef 字段符号引用 type FieldRef struct { MemberRef // 继承成员引用属性 field *Field // 解析后的字段(直接引用,运行时赋值) }三、类加载器:类的 “加载器” 与 “入口”类加载器负责将 Class 文件加载到 JVM 中,通过 “加载→链接→初始化” 过程将类信息存入方法区,是类进入运行时的关键组件。1. 类加载器核心逻辑// ClassLoader 类加载器 type ClassLoader struct { cp *classpath.Classpath // 类路径(用于查找 Class 文件) classMap map[string]*Class // 已加载类的缓存(类名→Class 实例) } // LoadClass 加载指定类(核心入口方法) func (c *ClassLoader) LoadClass(name string) *Class { // 1. 检查缓存,若已加载则直接返回 if class, ok := c.classMap[name]; ok { return class } // 2. 未加载则加载非数组类(数组类由 JVM 直接创建,此处简化) return c.loadNonArrayClass(name) } // loadNonArrayClass 加载非数组类的完整流程 func (c *ClassLoader) loadNonArrayClass(name string) *Class { // 步骤 1:读取 Class 文件字节流 data, entry := c.readClass(name) // 步骤 2:解析字节流为 Class 实例(加载阶段) class := c.defineClass(data) // 步骤 3:链接(验证、准备、解析) link(class) fmt.Printf("[Loaded %s from %s]\n", name, entry) return class }类加载核心步骤:加载:读取 Class 文件字节流,解析为 Class 实例;链接:验证:确保 Class 文件格式合法;准备:为静态变量分配内存并设置默认值(如 int 默认为 0);解析:将符号引用转化为直接引用(如将类符号引用指向已加载的类);初始化:执行类构造器 <clinit>() 方法(初始化静态变量和静态代码块)。四、对象、实例变量与类变量对象是类的实例,存储实例变量;类变量(静态变量)属于类,存储在方法区。JVM 通过槽位(Slots)管理变量的存储。1. 对象的结构// Object 表示 JVM 中的对象实例 type Object struct { class *Class // 对象所属的类 fields Slots // 实例变量(存储对象的字段值,大小由类的 instanceSlotCount 决定) } // newObject 创建对象实例 func newObject(c *Class) *Object { return &Object{ class: c, fields: newSlots(c.instanceSlotCount), // 按实例字段数量分配槽位 } }说明:对象的 fields 数组存储实例变量,每个字段对应一个槽位(long 和 double 占 2 个槽位),槽位索引由 Field.slotId 确定。2. 类变量与实例变量的区别类型存储位置生命周期访问方式示例实例变量对象的 fields与对象一致通过对象引用访问obj.name类变量类的 staticVars与类一致通过类名或对象引用访问Class.count 或 obj.count五、符号引用解析:从 “符号” 到 “实际地址”符号引用是编译期的间接引用,需在运行时解析为直接引用(内存中的实际对象 / 字段 / 方法)才能使用。1. 类符号引用解析// ResolveClass 将类符号引用解析为直接引用(Class 实例) func (s *SymRef) ResolveClass() *Class { if s.class == nil { s.resolveClassRef() // 未解析则执行解析 } return s.class } // resolveClassRef 实际执行类解析逻辑 func (s *SymRef) resolveClassRef() { d := s.cp.class // 引用所在的类(当前类) c := d.loader.LoadClass(s.className) // 加载目标类 // 检查访问权限(如当前类是否能访问目标类) if !c.isAccessibleTo(d) { panic("java.lang.IllegalAccessError") } s.class = c // 解析结果赋值给符号引用 }2. 字段符号引用解析// ResolvedField 将字段符号引用解析为直接引用(Field 实例) func (r *FieldRef) ResolvedField() *Field { if r.field == nil { r.resolveFieldRef() // 未解析则执行解析 } return r.field } // resolveFieldRef 实际执行字段解析逻辑 func (r *FieldRef) resolveFieldRef() { d := r.cp.class // 当前类 c := r.ResolveClass() // 解析字段所属的类 // 在类及其父类、接口中查找匹配的字段 field := lookupField(c, r.name, r.descriptor) if field == nil { panic("java.lang.NoSuchFieldError") } // 检查访问权限 if !field.isAccessibleTo(d) { panic("java.lang.IllegalAccessError") } r.field = field // 解析结果赋值 } // lookupField 在类、父类、接口中查找字段 func lookupField(c *Class, name string, descriptor string) *Field { // 1. 在当前类中查找 for _, field := range c.fields { if field.name == name && field.descriptor == descriptor { return field } } // 2. 在父类中递归查找 if c.superClass != nil { return lookupField(c.superClass, name, descriptor) } // 3. 在接口中查找 for _, iface := range c.interfaces { if field := lookupField(iface, name, descriptor); field != nil { return field } } return nil }六、类与对象相关指令实现JVM 提供专门的指令支持类与对象的操作,如创建对象、访问字段等。以下是核心指令的实现。1. NEW 指令:创建对象// NEW 创建类的实例对象 type NEW struct { base.Index16Instruction // 包含常量池索引(指向类符号引用) } func (n *NEW) Execute(frame *rtda.Frame) { cp := frame.Method().Class().ConstantPool() // 1. 从常量池获取类符号引用 classRef := cp.GetConstant(n.Index).(*ClassRef) // 2. 解析类符号引用,获取 Class 实例 class := classRef.ResolveClass() // 3. 检查类是否可实例化(非接口、非抽象类) if class.IsInterface() || class.IsAbstract() { panic("java.lang.InstantiationError") } // 4. 创建对象实例并推送至操作数栈 ref := class.NewObject() frame.OperandStack().PushRef(ref) } // NewObject 创建类的实例对象 func (c *Class) NewObject() *Object { return newObject(c) // 内部调用 newObject 分配对象内存 }2. PUT_STATIC 指令:设置静态字段值// PUT_STATIC 设置静态字段的值 func (p *PUT_STATIC) Execute(frame *rtda.Frame) { currentMethod := frame.Method() currentClass := currentMethod.Class() cp := currentClass.ConstantPool() // 1. 从常量池获取字段符号引用并解析 fieldRef := cp.GetConstant(p.Index).(*FieldRef) field := fieldRef.ResolvedField() class := field.Class() // 2. 检查字段合法性(必须是静态字段) if !field.IsStatic() { panic("java.lang.IncompatibleClassChangeError") } // 检查 final 字段的赋值权限(只能在 <clinit> 方法中赋值) if field.IsFinal() { if currentClass != class || currentMethod.Name() != "<clinit>" { panic("java.lang.IllegalAccessError") } } // 3. 根据字段类型从操作数栈弹出值,存入静态变量 descriptor := field.Descriptor() slotId := field.SlotId() slots := class.StaticVars() // 静态变量存储在类的 staticVars 中 stack := frame.OperandStack() // 根据字段描述符处理不同类型(int/float/long/double/引用) switch descriptor[0] { case 'Z', 'B', 'C', 'S', 'I': // 基本类型(boolean/byte/char/short/int) slots.SetInt(slotId, stack.PopInt()) case 'F': // float slots.SetFloat(slotId, stack.PopFloat()) case 'J': // long slots.SetLong(slotId, stack.PopLong()) case 'D': // double slots.SetDouble(slotId, stack.PopDouble()) case 'L', '[': // 引用类型(对象或数组) slots.SetRef(slotId, stack.PopRef()) } }七、功能测试:验证类加载与对象创建通过加载自定义类并执行对象创建、字段访问逻辑,验证类与对象机制的正确性。1. 测试代码与执行流程测试类:MyObject(包含静态字段和实例字段,简单逻辑用于验证);测试步骤:配置类路径,加载 MyObject 类;查找 main 方法并通过解释器执行;验证类加载、对象创建及字段访问是否正确。2. 测试结果执行命令 go install ./ch06/ && ch06 Myobject 后,日志显示类成功加载,且对象创建、字段赋值等操作正常执行:结论:方法区类信息存储、类加载流程、对象创建及 NEW/PUT_STATIC 等指令执行正确。本章小结本章实现了 JVM 中类与对象的核心机制,包括:方法区设计:通过 Class 结构体存储类元信息(字段、方法、常量池等);运行时常量池:管理字面量和符号引用,支持类与成员的关联;类加载器:完成类的加载、链接和初始化,将类信息载入方法区;对象模型:通过 Object 结构体存储实例变量,区分实例变量与类变量;核心指令:实现 NEW(创建对象)、PUT_STATIC(设置静态字段)等指令,支持类与对象的操作。下一章将完善方法调用机制(如 invokevirtual 指令)和继承关系处理,让 JVM 支持更复杂的面向对象特性。源码地址:https://github.com/Jucunqi/jvmgo.git
2025年06月20日
4 阅读
0 评论
0 点赞
2025-06-20
自己动手写 Java 虚拟机笔记 - 第五部分:指令集与解释器实现
前言在前一章中,我们实现了 JVM 运行时数据区(线程、栈帧、局部变量表、操作数栈等),为字节码执行提供了 “内存环境”。本章将聚焦 JVM 的指令集和解释器—— 指令集是字节码的 “操作命令”,解释器则负责将这些命令翻译成具体操作并执行,这是 JVM 执行程序的核心逻辑。参考资料《自己动手写 Java 虚拟机》—— 张秀宏开发环境工具 / 环境版本说明操作系统MacOS 15.5基于 Intel/Apple Silicon 均可JDK1.8用于字节码分析和测试Go 语言1.23.10项目开发主语言第五章:指令集与解释器核心实现JVM 通过字节码指令控制程序执行,每条指令对应特定的操作(如变量加载、算术运算、方法调用等)。解释器的作用是读取字节码,解析出指令并执行对应的操作,最终完成方法的逻辑。本章将实现常用指令集和基础解释器。一、指令集基础:常量池与核心结构回顾在解析指令前,需先回顾 Class 文件中与指令执行相关的核心结构,这些结构是指令操作的 “元数据” 来源。1. 常量池 Tag 对应关系常量池存储了指令执行所需的常量(字符串、类名、方法名等),每条常量通过 tag 字段标识类型。以下是核心常量类型的对应关系:Tag 值(十进制)Tag 值(十六进制)助记符说明10x01CONSTANT_Utf8UTF-8 编码的字符串常量(如类名、方法名)30x03CONSTANT_Integer整型常量40x04CONSTANT_Float浮点型常量50x05CONSTANT_Long长整型常量(占两个常量池条目)60x06CONSTANT_Double双精度浮点型常量(占两个常量池条目)70x07CONSTANT_Class类或接口的符号引用(指向类名)100x0aCONSTANT_Methodref类方法的符号引用(指向类和方法描述符)120x0cCONSTANT_NameAndType字段 / 方法的名称和描述符引用作用:指令执行时需通过常量池索引获取具体数据(如调用方法时通过 CONSTANT_Methodref 找到方法地址)。2. 核心结构定义指令的操作依赖 Class 文件中的字段表、方法表和属性表,以下是关键结构回顾:方法表(method_info):存储方法的访问标志、名称、描述符和属性(核心是 Code 属性,包含字节码)。method_info { u2 access_flags; // 方法访问标志(如 public、static) u2 name_index; // 方法名的常量池索引 u2 descriptor_index; // 方法描述符的常量池索引(如 "(I)V" 表示入参 int、返回 void) u2 attributes_count; // 属性数量 attribute_info attributes[attributes_count]; // 包含 Code 属性 }Code 属性:方法的核心属性,存储字节码指令、操作数栈大小、局部变量表大小等。Code_attribute { u2 attribute_name_index; // 指向 "Code" 字符串 u4 attribute_length; u2 max_stack; // 操作数栈最大深度 u2 max_locals; // 局部变量表大小 u4 code_length; // 字节码长度 u1 code[code_length]; // 字节码指令数组(核心执行内容) // 省略异常表和子属性... }作用:解释器通过 Code 属性获取字节码指令,结合 max_stack 和 max_locals 初始化栈帧。二、指令集分类与实现JVM 指令集包含数百条指令,按功能可分为常量加载、变量操作、算术运算、控制转移等类型。以下实现核心指令的关键逻辑。1. 基础指令(无操作 / 常量加载)nop 指令:无操作指令,用于字节码对齐(不执行任何操作)。// Nop 无操作指令 type Nop struct { base.NoOperandsInstruction // 无操作数指令基类 } func (n *Nop) Execute(frame *rtda.Frame) { // 空实现:仅占位,无实际操作 }const 指令:将常量推入操作数栈(如 aconst_null 推入 null 引用,iconst_0 推入 int 0)。// ACONST_NULL 推送 null 引用到操作数栈 type ACONST_NULL struct { base.NoOperandsInstruction } func (a *ACONST_NULL) Execute(frame *rtda.Frame) { frame.OperandStack().PushRef(nil) // 操作数栈推送 null } // ICONST_0 推送 int 0 到操作数栈 type ICONST_0 struct { base.NoOperandsInstruction } func (i *ICONST_0) Execute(frame *rtda.Frame) { frame.OperandStack().PushInt(0) // 操作数栈推送 int 0 }2. 常量推送指令(bipush/sipush)用于将小范围整数推入操作数栈(bipush 支持 8 位整数,sipush 支持 16 位整数)。// BIPUSH 推送 8 位整数到操作数栈 type BIPUSH struct { val int8 // 指令自带的 8 位常量值 } // 从字节码中读取操作数(8 位整数) func (b *BIPUSH) FetchOperands(reader *base.BytecodeReader) { b.val = int8(reader.ReadInt8()) } // 执行:推送常量到操作数栈 func (b *BIPUSH) Execute(frame *rtda.Frame) { frame.OperandStack().PushInt(int32(b.val)) } // SIPUSH 推送 16 位整数到操作数栈(逻辑类似,略) type SIPUSH struct { val int16 }3. 局部变量操作指令(加载 / 存储)加载指令(iload/iload_0):从局部变量表加载 int 类型到操作数栈(iload_0 是 iload 0 的简写,优化性能)。// ILOAD 从局部变量表加载 int(通过索引指定位置) type ILOAD struct { base.Index8Instruction // 包含 8 位索引字段 } func (i *ILOAD) Execute(frame *rtda.Frame) { // 从局部变量表 index 位置加载 int,推入操作数栈 index := i.Index val := frame.LocalVars().GetInt(index) frame.OperandStack().PushInt(val) } // ILOAD_0 从局部变量表 index 0 加载 int(简写指令,无操作数) type ILOAD_0 struct { base.NoOperandsInstruction } func (i *ILOAD_0) Execute(frame *rtda.Frame) { val := frame.LocalVars().GetInt(0) // 固定 index 0 frame.OperandStack().PushInt(val) }存储指令(istore/istore_0):从操作数栈弹出 int 类型到局部变量表(逻辑与加载指令相反)。// ISTORE 存储 int 到局部变量表 type ISTORE struct { base.Index8Instruction } func (i *ISTORE) Execute(frame *rtda.Frame) { val := frame.OperandStack().PopInt() // 从操作数栈弹出 frame.LocalVars().SetInt(i.Index, val) // 存入局部变量表 index 位置 }4. 栈操作指令(pop/dup/swap)操作数栈的元素管理指令,用于调整栈中数据顺序。pop 指令:弹出操作数栈顶元素(用于清理不需要的数据)。type POP struct { base.NoOperandsInstruction } func (p *POP) Execute(frame *rtda.Frame) { frame.OperandStack().PopSlot() // 弹出栈顶槽位(Slot) }swap 指令:交换操作数栈顶两个元素的位置(用于调整计算顺序)。// SWAP 交换栈顶两个元素(假设为 int 类型) type SWAP struct { base.NoOperandsInstruction } func (s *SWAP) Execute(frame *rtda.Frame) { stack := frame.OperandStack() slot1 := stack.PopSlot() // 弹出栈顶第一个元素 slot2 := stack.PopSlot() // 弹出栈顶第二个元素 stack.PushSlot(slot1) // 先推回第一个元素 stack.PushSlot(slot2) // 再推回第二个元素(完成交换) }5. 算术运算指令(iadd/ladd 等)对操作数栈中的元素执行算术运算,结果推回栈顶。// IADD 对操作数栈顶两个 int 相加 type IADD struct { base.NoOperandsInstruction } func (i *IADD) Execute(frame *rtda.Frame) { stack := frame.OperandStack() v2 := stack.PopInt() // 弹出第二个操作数 v1 := stack.PopInt() // 弹出第一个操作数 result := v1 + v2 // 计算 stack.PushInt(result) // 结果推回栈顶 } // LADD 对操作数栈顶两个 long 相加(逻辑类似,略) type LADD struct { base.NoOperandsInstruction }6. 控制转移指令(if/loop/tableswitch)改变程序执行流程,实现分支、循环等逻辑。if_acmpeq 指令:比较两个引用是否相等,相等则跳转。// IF_ACMPEQ 若两个引用相等则跳转 type IF_ACMPEQ struct { base.BranchInstruction // 包含跳转偏移量 Offset } func (i *IF_ACMPEQ) Execute(frame *rtda.Frame) { stack := frame.OperandStack() v2 := stack.PopRef() // 弹出第二个引用 v1 := stack.PopRef() // 弹出第一个引用 if v1 == v2 { base.Branch(frame, i.Offset) // 相等则跳转到 Offset 位置 } // 不相等则继续执行下一条指令 }tableswitch 指令:用于 switch-case 语句的连续整数匹配(高效跳转)。// TABLE_SWITCH 按整数索引跳转(适用于连续 case 值) type TABLE_SWITCH struct { defaultOffset int32 // 默认跳转偏移量 low int32 // case 最小值 high int32 // case 最大值 jumpOffsets []int32 // 每个 case 对应的跳转偏移量 } // 执行:根据栈顶整数选择跳转目标 func (t *TABLE_SWITCH) Execute(frame *rtda.Frame) { stack := frame.OperandStack() i := stack.PopInt() // 弹出 switch 的条件值 // 若值在 [low, high] 范围内,则跳转到对应偏移量 if i >= t.low && i <= t.high { index := i - t.low base.Branch(frame, int(t.jumpOffsets[index])) } else { base.Branch(frame, int(t.defaultOffset)) // 否则走默认分支 } }三、解释器实现解释器是连接字节码和运行时数据区的核心组件,负责:读取字节码指令→解析指令→执行指令操作→推进程序计数器。1. 指令工厂:根据 opcode 创建指令对象JVM 指令通过 opcode(操作码,1 字节) 区分类型,工厂类根据 opcode 生成对应指令实例。// NewInstruction 根据 opcode 创建指令对象 func NewInstruction(opcode byte) base.Instruction { switch opcode { case 0x00: // nop 指令 opcode return &Nop{} case 0x01: // aconst_null 指令 opcode return &ACONST_NULL{} case 0x10: // bipush 指令 opcode return &BIPUSH{} case 0x15: // iload 指令 opcode return &ILOAD{} case 0x60: // iadd 指令 opcode return &IADD{} case 0xa5: // if_acmpeq 指令 opcode return &IF_ACMPEQ{} // 省略其他指令... default: panic(fmt.Sprintf("未实现的指令 opcode: 0x%x", opcode)) } }2. 核心解释逻辑(interpret 方法)解释器的主流程:初始化运行时环境→循环读取字节码→执行指令→处理异常。// interpret 解释执行方法的字节码 func interpret(methodInfo *classfile.MemberInfo) { // 1. 从方法信息中获取 Code 属性(包含字节码和栈/变量表大小) codeAttr := methodInfo.CodeAttribute() maxLocals := codeAttr.MaxLocals() // 局部变量表大小 maxStack := codeAttr.MaxStack() // 操作数栈大小 bytecode := codeAttr.Code() // 字节码指令数组 // 2. 初始化运行时环境(线程、栈帧) thread := rtda.NewThread() // 创建线程 frame := thread.NewFrame(uint(maxLocals), uint(maxStack)) // 创建栈帧 thread.PushFrame(frame) // 栈帧入栈 // 3. 异常捕获:确保执行出错时打印信息 defer catchErr(frame) // 4. 循环执行字节码指令 loop(thread, bytecode) } // loop 循环读取并执行指令 func loop(thread *rtda.Thread, bytecode []byte) { frame := thread.CurrentFrame() reader := &base.BytecodeReader{} // 字节码读取器 for { // 获取当前程序计数器(指令地址) pc := frame.NextPC() thread.SetPC(pc) // 读取 opcode(1 字节) reader.Reset(bytecode, pc) opcode := reader.ReadUint8() // 创建指令对象并读取操作数 inst := NewInstruction(opcode) inst.FetchOperands(reader) // 更新程序计数器(指向 next 指令) frame.SetNextPC(reader.PC()) // 执行指令 fmt.Printf("pc: %d, opcode: 0x%x, inst: %T\n", pc, opcode, inst) inst.Execute(frame) } }核心逻辑:通过程序计数器(PC)定位当前指令,工厂类创建指令实例后执行,执行完成后更新 PC 指向下一步指令,形成循环。四、测试:执行 1-100 求和逻辑为验证指令集和解释器的正确性,我们通过一个简单的 Java 程序(1-100 求和)进行测试。1. 测试代码与字节码分析Java 测试类:public class GuessTest { public static void main(String[] args) { int result = 0; // 局部变量表 index 1(index 0 为 this) for (int i = 1; i <= 100; i++) { // i 在局部变量表 index 2 result += i; // 累加逻辑:result = result + i } } }字节码指令(循环累加部分):// 简化的字节码指令(核心逻辑) 0: iconst_0 // 推送 0 到操作数栈 1: istore_1 // 弹出 0 存入局部变量表 index 1(result = 0) 2: iconst_1 // 推送 1 到操作数栈 3: istore_2 // 弹出 1 存入局部变量表 index 2(i = 1) 4: iload_2 // 加载 i 到操作数栈 5: bipush 100 // 推送 100 到操作数栈 7: if_icmpgt 21 // 若 i > 100 则跳转到 21(退出循环) 10: iload_1 // 加载 result 到操作数栈 11: iload_2 // 加载 i 到操作数栈 12: iadd // result + i,结果推回栈顶 13: istore_1 // 弹出结果存入 result(更新 result) 14: iinc 2, 1 // i += 1(局部变量表 index 2 自增 1) 17: goto 4 // 跳转到 4(继续循环) 21: return // 方法返回(未实现,测试中会报错)手动解析二进制字节码2. 测试结果与验证执行测试命令:go install ./ch05/ && ch05,尽管因未实现 return 指令报错,但局部变量表中 result 的值已正确计算为 5050(1-100 求和结果)。结论:核心指令(iconst/istore/iadd/iinc/goto)执行正确,验证了解释器和指令集的有效性。本章小结本章实现了 JVM 指令集的核心逻辑和解释器,重点包括:指令集分类实现:常量加载、局部变量操作、算术运算、控制转移等指令,覆盖基础执行逻辑。解释器核心流程:通过指令工厂创建指令实例,循环读取字节码、执行指令并更新程序计数器。测试验证:通过 1-100 求和案例验证指令执行正确性,局部变量表结果符合预期。下一章将实现类和对象、体会类加载执行的过程。源码地址:https://github.com/Jucunqi/jvmgo.git
2025年06月20日
5 阅读
0 评论
0 点赞
2025-06-16
自己动手写 Java 虚拟机笔记 - 第四部分:实现运行时数据区
前言在前一章中,我们完成了 Class 文件的解析,获取了类的结构信息(字段、方法、常量等)。本章将聚焦 JVM 的运行时数据区—— 这是 JVM 执行字节码时存储数据和中间结果的核心区域,也是实现方法调用、变量存储的基础。参考资料《自己动手写 Java 虚拟机》—— 张秀宏开发环境工具 / 环境版本说明操作系统MacOS 15.5基于 Intel/Apple Silicon 均可JDK1.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
2025年06月16日
4 阅读
0 评论
0 点赞
2025-06-15
自己动手写 Java 虚拟机笔记 - 第三部分:解析 Class 文件核心结构
前言在前两章中,我们搭建了 JVM 的命令行入口和类路径查找逻辑。本章将深入 Class 文件的二进制结构,实现从字节流到结构化数据的解析,这是 JVM 加载类的核心步骤。参考资料《自己动手写 Java 虚拟机》—— 张秀宏开发环境工具 / 环境版本说明操作系统MacOS 15.5基于 Intel/Apple Silicon 均可JDK1.8用于字节码分析和测试Go 语言1.23.10项目开发主语言第三章:解析 Class 文件Class 文件是 Java 代码编译后的二进制文件,包含类的所有信息(类名、方法、字段、常量等)。JVM 通过解析 Class 文件才能理解并执行 Java 代码。本章将按 Class 文件的结构逐步实现解析逻辑。一、Class 文件整体结构概览Class 文件的结构严格遵循固定格式,通过不同长度的 “无符号整数”(u1/u2/u4 分别表示 1/2/4 字节无符号整数)描述数据。完整结构定义如下:ClassFile { u4 magic; // 魔数,固定值 0xCAFEBABE(验证文件合法性) u2 minor_version; // 次版本号(如 JDK 1.8 的 minor 为 0) u2 major_version; // 主版本号(如 JDK 1.8 的 major 为 52) u2 constant_pool_count; // 常量池大小(索引从 1 开始) cp_info constant_pool[constant_pool_count-1]; // 常量池(存储类中所有常量) u2 access_flags; // 类访问标志(如 public、final、abstract 等) u2 this_class; // 类本身的常量池索引(指向类名) u2 super_class; // 父类的常量池索引(指向父类名) u2 interfaces_count; // 实现的接口数量 u2 interfaces[interfaces_count]; // 接口列表(常量池索引数组) u2 fields_count; // 字段数量 field_info fields[fields_count]; // 字段表(存储类的成员变量信息) u2 methods_count; // 方法数量 method_info methods[methods_count]; // 方法表(存储类的方法信息) u2 attributes_count; // 属性数量 attribute_info attributes[attributes_count]; // 属性表(存储额外信息如代码、行号等) }核心目标:将上述二进制结构解析为 Go 语言中的结构体,便于后续 JVM 加载和执行。二、实现字节流读取工具(ClassReader)Class 文件本质是二进制字节流,我们需要一个工具类来按格式读取不同长度的数据(u1/u2/u4 等)。1. ClassReader 结构体与核心方法package classfile import "encoding/binary" // ClassReader 封装 Class 文件字节流的读取逻辑 type ClassReader struct { data []byte // 存储 Class 文件的二进制数据 } // 读取 1 字节无符号整数(u1) func (c *ClassReader) readUnit8() uint8 { val := c.data[0] // 取当前第一个字节 c.data = c.data[1:] // 移动指针到下一个字节 return val } // 读取 2 字节无符号整数(u2),大端字节序 func (c *ClassReader) readUnit16() uint16 { val := binary.BigEndian.Uint16(c.data) // 大端序解析 2 字节 c.data = c.data[2:] // 移动指针 return val } // 读取 4 字节无符号整数(u4),大端字节序 func (c *ClassReader) readUnit32() uint32 { val := binary.BigEndian.Uint32(c.data) c.data = c.data[4:] return val } // 读取 8 字节无符号整数(u8),大端字节序 func (c *ClassReader) readUnit64() uint64 { val := binary.BigEndian.Uint64(c.data) c.data = c.data[8:] return val } // 读取 u2 数组(先读长度,再读对应数量的 u2) func (c *ClassReader) readUnit16s() []uint16 { length := c.readUnit16() // 数组长度(u2) uint16s := make([]uint16, length) for i := range uint16s { uint16s[i] = c.readUnit16() // 依次读取每个元素 } return uint16s } // 读取指定长度的字节数组 func (c *ClassReader) readBytes(length uint32) []byte { bytes := c.data[:length] // 截取指定长度 c.data = c.data[length:] // 移动指针 return bytes }作用:ClassReader 屏蔽了字节流操作的细节,让上层解析逻辑更简洁,只需调用对应方法即可按格式读取数据。三、解析 Class 文件主结构(ClassFile)ClassFile 结构体对应 Class 文件的整体结构,负责协调解析魔数、版本号、常量池、字段、方法等核心部分。1. ClassFile 结构体定义package classfile import "fmt" // ClassFile 存储解析后的 Class 文件信息 type ClassFile struct { magic uint32 // 魔数(0xCAFEBABE) minorVersion uint16 // 次版本号 majorVersion uint16 // 主版本号 constantPool ConstantPool // 常量池(核心数据结构) accessFlags uint16 // 类访问标志 thisClass uint16 // 当前类的常量池索引 superClass uint16 // 父类的常量池索引 interfaces []uint16 // 接口索引列表 fields []*MemberInfo // 字段列表 methods []*MemberInfo // 方法列表 attributes []AttributeInfo // 属性列表 }2. 核心解析逻辑(Parse 方法)// Parse 从字节流解析 Class 文件 func Parse(classData []byte) (cf *ClassFile, err error) { // 捕获解析过程中的 panic(如格式错误),转为 error 返回 defer func() { if r := recover(); r != nil { var ok bool err, ok = r.(error) if !ok { err = fmt.Errorf("%v", r) // 非 error 类型的 panic 转为 error } } }() cr := &ClassReader{classData} // 创建字节流读取器 cf = &ClassFile{} cf.read(cr) // 开始解析 return } // read 按 Class 文件结构依次解析各部分 func (c *ClassFile) read(reader *ClassReader) { c.readAndCheckMagic(reader) // 验证魔数 c.readAndCheckVersion(reader) // 验证版本号 c.constantPool = readConstantPool(reader) // 解析常量池 c.accessFlags = reader.readUnit16() // 读取访问标志 c.thisClass = reader.readUnit16() // 读取当前类索引 c.superClass = reader.readUnit16() // 读取父类索引 c.interfaces = reader.readUnit16s() // 读取接口列表 c.fields = readMembers(reader, c.constantPool) // 解析字段 c.methods = readMembers(reader, c.constantPool) // 解析方法 c.attributes = readAttributes(reader, c.constantPool) // 解析属性 }3. 关键验证步骤魔数验证:确保文件是合法的 Class 文件(固定为 0xCAFEBABE)。func (c *ClassFile) readAndCheckMagic(reader *ClassReader) { magic := reader.readUnit32() if magic != 0xCAFEBABE { panic("java.lang.ClassFormatError: invalid magic number") } }版本号验证:JVM 只支持特定版本的 Class 文件(如 JDK 1.8 支持 45.0 ~ 52.0 版本)。func (c *ClassFile) readAndCheckVersion(reader *ClassReader) { c.minorVersion = reader.readUnit16() c.majorVersion = reader.readUnit16() // 支持 JDK 1.0.2(45.0)到 JDK 1.8(52.0)的版本 switch c.majorVersion { case 45: return // JDK 1.0.2 case 46, 47, 48, 49, 50, 51, 52: if c.minorVersion == 0 { return // JDK 1.1 ~ 1.8 } } panic("java.lang.UnsupportedClassVersionError") }四、解析常量池(ConstantPool)常量池是 Class 文件中最复杂的部分,存储类中所有常量(字符串、类名、方法名、字段名等),是解析其他结构的基础。1. 常量池结构与类型常量池由多个 cp_info 结构组成,每个结构以 tag 字段标识类型(共 17 种,如字符串、类引用、方法引用等)。核心类型如下:tag 值常量类型作用1ConstantUtf8Info存储 UTF-8 字符串(如类名、方法名)7ConstantClassInfo类或接口的引用(指向 UTF-8 类名)10ConstantMethodRefInfo方法引用(指向类和方法描述符)12ConstantNameAndTypeInfo名称和类型描述符(字段 / 方法的元信息)2. 常量池解析逻辑package classfile // ConstantPool 常量池(本质是常量接口数组) type ConstantPool []ConstantInfo // 读取常量池 func readConstantPool(reader *ClassReader) ConstantPool { cpCount := int(reader.readUnit16()) // 常量池大小(索引从 1 开始) cp := make([]ConstantInfo, cpCount) for i := 1; i < cpCount; i++ { cp[i] = readConstantInfo(reader, cp) // 解析单个常量 // 注意:Long 和 Double 类型占 2 个索引位置 switch cp[i].(type) { case *ConstantLongInfo, *ConstantDoubleInfo: i++ // 跳过下一个索引 } } return cp } // 从常量池获取指定索引的常量 func (c ConstantPool) getConstantInfo(index uint16) ConstantInfo { if cpInfo := c[index]; cpInfo != nil { return cpInfo } panic("invalid constant pool index") } // 工具方法:通过索引获取类名(从 ConstantClassInfo 中解析) func (c ConstantPool) getClassName(index uint16) string { classInfo := c.getConstantInfo(index).(*ConstantClassInfo) return c.getUtf8(classInfo.nameIndex) // 类名存储在 UTF-8 常量中 } // 工具方法:通过索引获取 UTF-8 字符串 func (c ConstantPool) getUtf8(index uint16) string { utf8Info := c.getConstantInfo(index).(*ConstantUtf8Info) return utf8Info.str }关键设计:通过 ConstantInfo 接口抽象不同类型的常量,每种常量类型实现接口的 readInfo 方法,实现多态解析。五、解析字段和方法(MemberInfo)字段(Field)和方法(Method)的结构相似,都包含访问标志、名称索引、描述符索引和属性列表,因此可以用同一个 MemberInfo 结构体封装。1. MemberInfo 结构体package classfile // MemberInfo 封装字段或方法的信息 type MemberInfo struct { cp ConstantPool // 常量池(用于解析名称和描述符) accessFlags uint16 // 访问标志(如 public、private、static 等) nameIndex uint16 // 名称的常量池索引(字段名/方法名) descriptorIndex uint16 // 描述符的常量池索引(类型信息) attributes []AttributeInfo // 属性列表(如字段的常量值、方法的代码等) } // 读取字段或方法列表 func readMembers(reader *ClassReader, cp ConstantPool) []*MemberInfo { memberCount := reader.readUnit16() // 成员数量 members := make([]*MemberInfo, memberCount) for i := range members { members[i] = readMember(reader, cp) // 解析单个成员 } return members } // 读取单个字段或方法 func readMember(reader *ClassReader, cp ConstantPool) *MemberInfo { return &MemberInfo{ cp: cp, accessFlags: reader.readUnit16(), nameIndex: reader.readUnit16(), descriptorIndex: reader.readUnit16(), attributes: readAttributes(reader, cp), } } // 工具方法:获取字段/方法名称 func (m *MemberInfo) Name() string { return m.cp.getUtf8(m.nameIndex) } // 工具方法:获取字段/方法描述符(如 "Ljava/lang/String;" 表示字符串类型) func (m *MemberInfo) Descriptor() string { return m.cp.getUtf8(m.descriptorIndex) }示例:解析 java.lang.String 类的方法时,nameIndex 指向常量池中的 "length" 字符串,descriptorIndex 指向 "()I"(表示无参、返回 int 的方法)。六、解析属性(AttributeInfo)属性是 Class 文件中存储额外信息的结构,不同属性有不同的格式(如方法的字节码存储在 Code 属性中,行号映射存储在 LineNumberTable 属性中)。1. 属性接口与解析逻辑package classfile // AttributeInfo 属性接口(所有属性类型都需实现此接口) type AttributeInfo interface { readInfo(reader *ClassReader) // 读取属性具体内容 } // 读取属性列表 func readAttributes(reader *ClassReader, cp ConstantPool) []AttributeInfo { attributeCount := reader.readUnit16() // 属性数量 attributes := make([]AttributeInfo, attributeCount) for i := range attributes { attributes[i] = readAttribute(reader, cp) // 解析单个属性 } return attributes } // 读取单个属性(根据属性名创建对应类型的实例) func readAttribute(reader *ClassReader, cp ConstantPool) AttributeInfo { attrNameIndex := reader.readUnit16() // 属性名的常量池索引 attrName := cp.getUtf8(attrNameIndex) // 获取属性名(如 "Code"、"SourceFile") attrLen := reader.readUnit32() // 属性长度(内容字节数) attribute := newAttribute(attrName, attrLen, cp) // 创建属性实例 attribute.readInfo(reader) // 读取属性内容 return attribute } // 根据属性名创建对应类型的实例 func newAttribute(attrName string, attrLen uint32, cp ConstantPool) AttributeInfo { switch attrName { case "Code": return &CodeAttribute{cp: cp} // 方法的字节码属性 case "ConstantValue": return &ConstantValueAttribute{} // 字段的常量值属性 case "Exceptions": return &ExceptionAttribute{} // 方法抛出的异常属性 case "LineNumberTable": return &LineNumberTableAttribute{} // 字节码与行号映射属性 case "SourceFile": return &SourceFileAttribute{cp: cp} // 源文件名属性 default: // 未实现的属性用 UnparsedAttribute 存储原始数据 return &UnparsedAttribute{attrName, attrLen, nil} } }核心属性示例:Code 属性:存储方法的字节码指令、操作数栈大小、局部变量表大小等核心执行信息。SourceFile 属性:记录类对应的源文件名(如 String.java)。七、测试解析逻辑1. 修改启动函数验证解析结果在 main.go 中添加加载类并打印解析结果的逻辑:func startJVM(cmd *Cmd) { // 解析类路径 cp := classpath.Parse(cmd.xJreOption, cmd.cpOption) // 加载类(如 java.lang.String) classname := strings.ReplaceAll(cmd.class, ".", "/") // 转为类文件路径(如 "java/lang/String") class := loadClass(classname, cp) printClass(class) // 打印解析结果 } // 打印类的核心信息 func printClass(cf *classfile.ClassFile) { fmt.Printf("version: %v.%v\n", cf.MajorVersion(), cf.MinorVersion()) fmt.Printf("constants count: *s", len(cf.ConstantPool())) fmt.Printf("access flag: 0x%x\n", cf.AccessFlags()) fmt.Printf("this class: %s\n", cf.ClassName()) fmt.Printf("super class: %s\n", cf.SuperClassName()) fmt.Printf("interfaces name: %s\n", cf.InterfaceNames()) fmt.Printf("filed count: %s\n", len(cf.Fields())) for _, field := range cf.Fields() { fmt.Printf("%s\n", field.Name()) } fmt.Printf("method count: %s\n", len(cf.Methods())) for _, method := range cf.Methods() { fmt.Printf("%s\n", method.Name()) } }2. 执行测试并验证结果编译命令:go install ./ch03/执行命令:ch03 java.lang.String预期输出:打印 java.lang.String 类的版本号、类名、父类名(java.lang.Object)、字段(如 value)、方法(如 length、equals)等信息。本章小结本章完成了 Class 文件的核心解析逻辑,重点包括:用 ClassReader 封装字节流读取,按格式解析 u1/u2/u4 等数据;实现 ClassFile 结构体,按固定结构解析魔数、版本号、常量池等核心部分;通过接口抽象常量池和属性的多态解析,适配不同类型的常量和属性;用 MemberInfo 统一封装字段和方法的解析逻辑。下一章将基于解析后的 Class 文件信息,实现运行时数据区,进一步理解JVM内部的奥秘。源码地址:https://github.com/Jucunqi/jvmgo.git
2025年06月15日
11 阅读
0 评论
0 点赞
1
2
3
4