自己动手写 Java 虚拟机笔记 - 第十部分：异常处理机制实现（系列终章）
前言在前一章中，我们实现了本地方法调用与反射机制，让 JVM 具备了与底层交互和动态访问类信息的能力。本章将聚焦 JVM 的 异常处理机制—— 这是保障程序健壮性的核心功能。Java 异常分为 Checked 异常和 Unchecked 异常，通过 throw 关键字抛出，依赖异常处理表和 athrow 指令实现捕获与处理。本章将完整实现异常的抛出、捕获逻辑，以及堆栈跟踪功能，并通过测试验证异常处理的正确性。作为系列笔记的终章，本章结尾还将对整个 JVM 实现之旅进行总结。参考资料《自己动手写 Java 虚拟机》—— 张秀宏开发环境工具 / 环境版本说明操作系统MacOS 15.5基于 Intel/Apple Silicon 均可JDK1.8用于字节码分析和测试Go 语言1.23.10项目开发主语言第十章：异常处理机制实现异常处理是 Java 语言的重要特性，允许程序在运行时捕获并处理错误，而非直接崩溃。JVM 通过异常处理表记录捕获逻辑，通过 athrow 指令抛出异常，并在栈中查找合适的处理程序。本章将实现这一完整流程。一、异常概述：类型与继承关系Java 中的所有异常都继承自 java.lang.Throwable，按是否必须捕获分为两类：异常类型定义示例Checked 异常非 RuntimeException 和 Error 的子类，必须显式捕获或声明抛出IOException、ClassNotFoundExceptionUnchecked 异常包括 RuntimeException 及其子类（运行时异常）和 Error 及其子类（错误），无需显式捕获NullPointerException、OutOfMemoryError继承关系核心：java.lang.Object └── java.lang.Throwable ├── java.lang.Error（错误，如 StackOverflowError） └── java.lang.Exception（异常） ├── Checked 异常（如 IOException） └── java.lang.RuntimeException（运行时异常，Unchecked）二、异常抛出：throw 关键字与 athrow 指令在 Java 代码中，通过 throw 关键字抛出异常，对应字节码中的 athrow 指令，负责将异常对象从操作数栈弹出并触发异常处理流程。1. athrow 指令的核心逻辑athrow 指令的执行流程：从操作数栈弹出异常对象引用（必须是非 null 的 Throwable 实例）；遍历当前线程的栈帧，在每个方法的异常处理表中查找匹配的异常处理程序；找到处理程序后，清空当前栈帧的操作数栈，将异常对象推入栈顶，跳转到处理程序执行；若遍历所有栈帧仍未找到处理程序，则终止线程并输出堆栈跟踪。// ATHROW 异常抛出指令 type ATHROW struct { base.NoOperandsInstruction } func (a *ATHROW) Execute(frame *rtda.Frame) { // 1. 从操作数栈弹出异常对象 ex := frame.OperandStack().PopRef() if ex == nil { panic("java.lang.NullPointerException") // 不能抛出 null } thread := frame.Thread() // 2. 查找异常处理程序 if !findAndGotoExceptionHandler(thread, ex) { // 3. 未找到处理程序，输出堆栈并终止线程 handleUncaughtException(thread, ex) } }三、异常处理表：捕获逻辑的存储结构每个方法的 Code 属性中包含异常处理表（exception_table），记录该方法中异常捕获的范围、类型和处理程序位置，是异常捕获的核心依据。1. 异常处理表的结构// ExceptionHandler 异常处理表中的一项 type ExceptionHandler struct { startPc int // 异常监控的起始 PC 地址（包含） endPc int // 异常监控的结束 PC 地址（不包含） handlerPc int // 异常处理程序的 PC 地址（跳转目标） catchType *ClassRef // 捕获的异常类型（null 表示捕获所有异常，对应 catch (Throwable)） } // ExceptionTable 异常处理表（由多个 ExceptionHandler 组成） type ExceptionTable []*ExceptionHandler字段说明：startPc 和 endPc：定义监控的代码范围（[startPc, endPc)），该范围内抛出的异常会被当前处理程序监控；handlerPc：当异常被捕获时，程序计数器跳转至此地址执行处理逻辑；catchType：指定捕获的异常类型（通过常量池中的类符号引用），null 表示捕获所有异常（对应 catch (Throwable)）。2. 异常处理程序的查找逻辑当异常抛出后，JVM 需要在当前方法的异常处理表中查找最合适的处理程序：// findExceptionHandler 查找匹配的异常处理程序 func (t ExceptionTable) findExceptionHandler(exClass *Class, pc int) *ExceptionHandler { for _, handler := range t { // 1. 检查当前 PC 是否在监控范围内（[startPc, endPc)） if pc >= handler.startPc && pc < handler.endPc { // 2. 若捕获所有异常（catchType 为 null），直接返回 if handler.catchType == nil { return handler } // 3. 解析捕获的异常类型，检查是否与抛出的异常兼容 catchClass := handler.catchType.ResolveClass() if catchClass == exClass || exClass.IsSubClassOf(catchClass) { // 异常类型匹配（抛出的异常是捕获类型或其子类） return handler } } } return nil // 未找到匹配的处理程序 }匹配规则：优先匹配范围包含当前 PC 且异常类型兼容的处理程序；若存在多个匹配的处理程序，按在异常处理表中的顺序优先选择第一个。四、异常处理流程：从抛出到捕获异常处理的完整流程涉及栈帧遍历、处理程序查找和流程跳转，确保异常被正确捕获或向上传播。1. 查找并执行异常处理程序// findAndGotoExceptionHandler 在栈中查找异常处理程序并跳转 func findAndGotoExceptionHandler(thread *rtda.Thread, ex *heap.Object) bool { for { // 1. 获取当前栈顶栈帧 frame := thread.CurrentFrame() // 当前指令的 PC（抛出异常的位置） pc := frame.NextPC() - 1 // 2. 在当前方法的异常处理表中查找处理程序 handler := frame.Method().ExceptionTable().findExceptionHandler(ex.Class(), pc) if handler != nil { // 3. 找到处理程序：清空操作数栈，推送异常对象，跳转执行 stack := frame.OperandStack() stack.Clear() stack.PushRef(ex) frame.SetNextPC(handler.handlerPc) return true } // 4. 未找到，弹出当前栈帧，继续在调用栈中查找 thread.PopFrame() // 5. 若栈为空，说明未找到任何处理程序 if thread.IsStackEmpty() { break } } return false }流程说明：从抛出异常的方法开始，逐层遍历调用栈（弹出栈帧），在每个方法的异常处理表中查找匹配的处理程序；找到后，清空当前栈帧的操作数栈，将异常对象推入栈顶，设置程序计数器为 handlerPc 执行处理逻辑；若遍历所有栈帧仍未找到处理程序，则该异常为 “未捕获异常”，触发线程终止。五、堆栈跟踪：fillInStackTrace 本地方法当异常未被捕获时，JVM 需要输出堆栈跟踪信息（包含异常类型、消息和调用栈），帮助定位问题。这一功能通过 Throwable.fillInStackTrace() 本地方法实现。1. 堆栈跟踪元素的结构堆栈跟踪由多个 StackTraceElement 组成，每个元素记录调用栈中的一个方法信息：// StackTraceElement 堆栈跟踪元素 type StackTraceElement struct { fileName string // 文件名（如 "ParseIntTest.java"） className string // 类名（如 "ParseIntTest"） methodName string // 方法名（如 "bar"） lineNumber int // 行号（-1 表示未知） }2. fillInStackTrace 实现该方法填充异常的堆栈信息，记录从异常抛出点到线程启动的完整调用栈：// 注册本地方法：java/lang/Throwable.fillInStackTrace() func init() { native.Register("java/lang/Throwable", "fillInStackTrace", "(I)Ljava/lang/Throwable;", fillInStackTrace) } // fillInStackTrace 填充异常的堆栈跟踪信息 func fillInStackTrace(frame *rtda.Frame) { this := frame.LocalVars().GetThis() // 获取 Throwable 实例 // 从当前线程的栈帧中收集堆栈信息 stacks := collectStackTraceElements(frame.Thread(), this) // 将堆栈信息存储到异常对象中（通过 extra 字段） this.SetExtra(stacks) frame.OperandStack().PushRef(this) // 返回异常对象本身 } // collectStackTraceElements 收集堆栈跟踪元素 func collectStackTraceElements(thread *rtda.Thread, ex *heap.Object) []*StackTraceElement { var elements []*StackTraceElement // 遍历线程的栈帧（跳过 fillInStackTrace 方法本身的栈帧） for frame := thread.CurrentFrame().Lower(); frame != nil; frame = frame.Lower() { method := frame.Method() class := method.Class() // 创建堆栈元素：包含类名、方法名、文件名和行号 element := &StackTraceElement{ className: class.JavaName(), methodName: method.Name(), fileName: class.SourceFile(), // 从类的 SourceFile 属性获取文件名 lineNumber: method.GetLineNumber(frame.NextPC() - 1), // 获取当前 PC 对应的行号 } elements = append(elements, element) } return elements }功能：通过遍历线程的栈帧，收集每个方法的类名、方法名、文件名和行号，最终存储到异常对象中，为后续打印堆栈跟踪提供数据。六、测试：异常处理全流程验证通过 ParseIntTest 测试类验证异常的抛出、捕获和堆栈跟踪功能：1. 测试代码public class ParseIntTest { public static void main(String[] args) { foo(args); // 调用 foo 方法 } private static void foo(String[] args) { try { bar(args); // 调用 bar 方法，可能抛出异常 } catch (NumberFormatException e) { // 捕获数字格式化异常 System.out.println("捕获 NumberFormatException：" + e.getMessage()); } } private static void bar(String[] args) { if (args.length == 0) { // 若没有参数，抛出索引越界异常 throw new IndexOutOfBoundsException("没有输入参数！"); } // 尝试将参数转换为整数（可能抛出 NumberFormatException） int x = Integer.parseInt(args[0]); System.out.println("解析结果：" + x); } }2. 测试场景与结果场景 1：无参数运行（java ParseIntTest）→ bar 方法抛出 IndexOutOfBoundsException，未被 foo 的 NumberFormatException 捕获，向上传播至 main 方法，最终输出堆栈跟踪。场景 2：参数为非数字（java ParseIntTest abc）→ Integer.parseInt 抛出 NumberFormatException，被 foo 的 catch 块捕获并处理。测试结果：两种场景均按预期执行，异常捕获逻辑和堆栈输出正确。系列总结：自己动手写 JVM 的旅程从第一部分的命令行工具到本章的异常处理，我们完成了一个简易 JVM 的核心功能实现。回顾整个系列，我们走过了以下关键旅程：1. 基础搭建（第一、二章）实现命令行参数解析，作为 JVM 的入口；设计类路径查找逻辑，支持从 JAR 包、目录加载 Class 文件。2. 类加载与解析（第三、六章）解析 Class 文件结构，提取魔数、版本号、常量池、字段、方法等信息；实现方法区存储类元信息，通过类加载器完成 “加载→链接→初始化” 流程；解析符号引用为直接引用，建立类、字段、方法的运行时关联。3. 运行时数据区（第四、五章）实现线程、虚拟机栈、栈帧、局部变量表、操作数栈等核心结构；设计指令集和解释器，支持常量加载、算术运算、控制转移等基础指令；实现方法调用与返回机制，支持静态绑定和动态绑定（多态）。4. 复杂数据结构（第七、八章）实现数组的动态创建和操作指令，支持基本类型和引用类型数组；通过字符串池实现字符串常量的共享，支持字符串拼接和 intern 机制。5. 扩展能力（第九、十章）设计本地方法注册与调用框架，实现反射核心功能和类库依赖的本地方法；完整实现异常处理机制，支持异常抛出、捕获和堆栈跟踪。收获与展望通过亲手实现 JVM，我们深入理解了 “Write once, run anywhere” 的底层逻辑：从 Class 文件的二进制结构到指令执行的每一个细节，从内存管理到异常处理，每一部分都是对计算机体系结构和面向对象思想的深度实践。这个简易 JVM 仍有许多可扩展之处（如 JIT 编译、垃圾回收、并发支持等），但已覆盖核心功能，足以执行简单的 Java 程序。希望这份笔记能为同样对 JVM 原理感兴趣的开发者提供参考，让我们在探索技术底层的道路上继续前行。源码地址：https://github.com/Jucunqi/jvmgo.git
自己动手写 Java 虚拟机笔记 - 第九部分：本地方法调用与反射机制实现
前言在前一章中，我们实现了数组和字符串的核心机制，完善了 JVM 对复杂数据结构的支持。本章将聚焦 本地方法调用 与反射机制 —— 本地方法（native 方法）是 Java 与底层系统交互的桥梁（如调用操作系统 API、硬件驱动等），而反射机制则依赖本地方法实现类信息的动态访问（如动态获取类结构、调用方法）。本章将通过 Go 语言模拟本地方法的注册、调用逻辑，实现反射的核心功能，并验证关键场景（如字符串拼接、类信息获取），让 JVM 具备与底层交互和动态操作类的能力。参考资料《自己动手写 Java 虚拟机》—— 张秀宏开发环境工具 / 环境版本说明操作系统MacOS 15.5基于 Intel/Apple Silicon 均可JDK1.8用于字节码分析和测试Go 语言1.23.10项目开发主语言第九章：本地方法调用与反射机制本地方法是 Java 语言扩展能力的关键，允许开发者通过其他语言（如 C/C++）实现底层功能；反射则基于本地方法实现类信息的动态访问。本章将从本地方法的注册、调用逻辑入手，逐步实现反射机制，并验证核心场景的正确性。一、本地方法基础：注册与调用机制本地方法（native 方法）没有 Java 字节码实现，需通过外部语言实现并注册到 JVM 中。JVM 需提供注册机制和调用逻辑，确保能正确找到并执行本地方法。1. 本地方法注册：建立方法映射表本地方法通过 “类名 + 方法名 + 方法描述符” 唯一标识，使用 map 存储方法映射关系（key 为标识，value 为 Go 实现的函数）。// NativeMethod 定义本地方法的函数类型（接收栈帧，无返回值） type NativeMethod func(frame *rtda.Frame) // registry 存储本地方法映射：key 为 "类名~方法名~描述符"，value 为本地方法实现 var registry = map[string]NativeMethod{} // Register 注册本地方法 func Register(className string, methodName string, methodDescriptor string, method NativeMethod) { key := className + "~" + methodName + "~" + methodDescriptor registry[key] = method }key 设计逻辑：类名、方法名、描述符共同构成唯一标识，避免不同类中同名方法的冲突（如 java/lang/System.arraycopy 与 java/util/Arrays.arraycopy 需区分）。示例：java/lang/System~arraycopy~(Ljava/lang/Object;ILjava/lang/Object;II)V 标识 System.arraycopy 方法。2. 本地方法调用：从字节码到本地实现JVM 通过 invokenative 指令调用本地方法，核心流程为：解析方法标识→查找本地实现→执行本地函数。（1）注入本地方法的 “伪字节码”本地方法无 Code 属性，需为其注入最小化字节码（用于解释器流程兼容）：// injectCodeAttribute 为本地方法注入伪 Code 属性 func (m *Method) injectCodeAttribute(returnType string) { m.maxStack = 4 // 操作数栈默认深度 m.maxLocals = m.argSlotCount // 局部变量表大小=参数槽数 // 根据返回类型生成伪字节码（首字节 0xFE 标识本地方法，第二字节为返回指令） switch returnType[0] { case 'V': // void 返回 m.code = []byte{0xfe, 0xb1} // 0xFE=本地方法标识，0xB1=return 指令 case 'D': // double 返回 m.code = []byte{0xfe, 0xaf} // 0xAF=dreturn 指令 case 'F': // float 返回 m.code = []byte{0xfe, 0xae} // 0xAE=freturn 指令 case 'J': // long 返回 m.code = []byte{0xfe, 0xad} // 0xAD=lreturn 指令 case 'L', '[': // 引用类型返回 m.code = []byte{0xfe, 0xb0} // 0xB0=areturn 指令 default: // 基本类型（int/short等）返回 m.code = []byte{0xfe, 0xac} // 0xAC=ireturn 指令 } }设计目的：确保解释器能正常解析方法结构，通过 0xFE 标识触发本地方法调用逻辑。（2）invokenative 指令执行逻辑// INVOKE_NATIVE 调用本地方法的指令 type INVOKE_NATIVE struct { base.NoOperandsInstruction } func (i *INVOKE_NATIVE) Execute(frame *rtda.Frame) { method := frame.Method() className := method.Class().Name() methodName := method.Name() descriptor := method.Descriptor() // 查找本地方法实现 nativeMethod := native.FindNativeMethod(className, methodName, descriptor) if nativeMethod == nil { // 未找到本地方法时抛出异常 panic("java.lang.UnsatisfiedLinkError: " + className + "." + methodName + descriptor) } // 执行本地方法 nativeMethod(frame) } // FindNativeMethod 从注册表查找本地方法 func FindNativeMethod(className, methodName, descriptor string) NativeMethod { key := className + "~" + methodName + "~" + descriptor if method, ok := registry[key]; ok { return method } // 特殊处理：对未实现的 native 方法返回默认实现（如 Object.registerNatives） if methodName == "registerNatives" && descriptor == "()V" { return func(frame *rtda.Frame) {} // 空实现 } return nil }调用流程：从当前栈帧获取方法的类名、方法名、描述符；生成 key 并查找本地方法实现；执行找到的本地函数（传入栈帧，操作局部变量和操作数栈）。二、反射机制实现：基于本地方法的动态类访问反射允许程序在运行时动态获取类信息（如类名、方法、字段）并操作，其核心依赖 java/lang/Class 类（类对象）和相关本地方法。1. 类对象（java/lang/Class 实例）的绑定每个类在 JVM 中对应唯一的 Class 实例（类对象），存储类的元信息，是反射的入口。// Class 结构体新增类对象字段 type Class struct { // ... 原有字段 ... jClass *Object // 对应的 java/lang/Class 实例（类对象） } // 类加载时绑定类对象 func (c *ClassLoader) LoadClass(name string) *Class { // ... 原有加载逻辑 ... // 绑定类对象：当 java/lang/Class 类已加载时 if jlClassClass, ok := c.classMap["java/lang/Class"]; ok { class.jClass = jlClassClass.NewObject() // 创建 Class 实例 class.jClass.extra = class // 关联到当前类（通过 extra 字段存储元信息） } return class }类对象的作用：作为反射的入口（如 obj.getClass() 返回类对象）；存储类的元信息（通过 extra 字段关联到 JVM 内部的 Class 结构体）。2. 核心反射本地方法实现反射的关键操作（如获取类名、获取类对象）依赖本地方法实现，以下是核心方法的 Go 实现。（1）Object.getClass()：获取对象的类对象// 注册本地方法：java/lang/Object.getClass() func init() { native.Register("java/lang/Object", "getClass", "()Ljava/lang/Class;", getClass) } // getClass 实现：返回对象的类对象 func getClass(frame *rtda.Frame) { this := frame.LocalVars().GetThis() // 获取当前对象（this） class := this.Class().JClass() // 获取类对象（jClass 字段） frame.OperandStack().PushRef(class) // 推送类对象到操作数栈 }（2）Class.getName0()：获取类的名称// 注册本地方法：java/lang/Class.getName0() func init() { native.Register("java/lang/Class", "getName0", "()Ljava/lang/String;", getName0) } // getName0 实现：返回类的全限定名 func getName0(frame *rtda.Frame) { this := frame.LocalVars().GetThis() // 获取 Class 实例（类对象） class := this.Extra().(*heap.Class) // 从 extra 字段获取 JVM 内部 Class 结构体 name := class.JavaName() // 转换类名为 Java 格式（如 "[I" → "int[]"） jString := heap.JString(class.Loader(), name) // 转换为 Java String 对象 frame.OperandStack().PushRef(jString) // 推送结果到操作数栈 } // JavaName 将 JVM 类名转换为 Java 规范名称 func (c *Class) JavaName() string { if c.IsArray() { return c.name // 数组类名已符合规范（如 "[I"） } return strings.ReplaceAll(c.name, "/", ".") // 普通类名："java/lang/String" → "java.lang.String" }（3）Class.getPrimitiveClass()：获取基本类型的类对象// 注册本地方法：java/lang/Class.getPrimitiveClass() func init() { native.Register("java/lang/Class", "getPrimitiveClass", "(Ljava/lang/String;)Ljava/lang/Class;", getPrimitiveClass) } // getPrimitiveClass 实现：返回基本类型的类对象 func getPrimitiveClass(frame *rtda.Frame) { vars := frame.LocalVars() nameObj := vars.GetRef(0) // 获取基本类型名称（如 "int"） name := heap.GoString(nameObj) // 转换为 Go 字符串 loader := frame.Method().Class().Loader() var class *heap.Class switch name { case "void": class = loader.LoadClass("void") case "boolean": class = loader.LoadClass("boolean") // ... 其他基本类型 ... default: panic("Invalid primitive type: " + name) } frame.OperandStack().PushRef(class.JClass()) // 推送基本类型的类对象 }三、核心本地方法案例：数组拷贝与字符串操作除反射外，Java 类库中的许多基础功能依赖本地方法，如数组拷贝、字符串拼接等。以下实现关键场景的本地方法。1. System.arraycopy()：数组拷贝// 注册本地方法：java/lang/System.arraycopy() func init() { native.Register("java/lang/System", "arraycopy", "(Ljava/lang/Object;ILjava/lang/Object;II)V", arraycopy) } // arraycopy 实现：数组元素拷贝 func arraycopy(frame *rtda.Frame) { vars := frame.LocalVars() src := vars.GetRef(0) // 源数组 srcPos := vars.GetInt(1) // 源数组起始位置 dest := vars.GetRef(2) // 目标数组 destPos := vars.GetInt(3)// 目标数组起始位置 length := vars.GetInt(4) // 拷贝长度 // 校验：源/目标数组非空 if src == nil || dest == nil { panic("java.lang.NullPointerException") } // 校验：数组类型兼容 if !checkArrayCopy(src, dest) { panic("java.lang.ArrayStoreException") } // 校验：索引不越界 if srcPos < 0 || destPos < 0 || length < 0 || srcPos+length > src.ArrayLength() || destPos+length > dest.ArrayLength() { panic("java.lang.IndexOutOfBoundsException") } // 执行拷贝（根据数组类型调用对应拷贝逻辑） heap.ArrayCopy(src, dest, srcPos, destPos, length) } // 校验数组拷贝的类型兼容性 func checkArrayCopy(src, dest *heap.Object) bool { srcClass, destClass := src.Class(), dest.Class() // 必须都是数组 if !srcClass.IsArray() || !destClass.IsArray() { return false } // 基本类型数组必须类型相同；引用类型数组允许子类向父类拷贝 if srcClass.ComponentClass().IsPrimitive() || destClass.ComponentClass().IsPrimitive() { return srcClass == destClass // 基本类型数组必须同类型 } return true // 引用类型数组兼容 }2. 字符串拼接与 String.intern()字符串拼接依赖 StringBuilder.append()，而 append 又依赖 System.arraycopy；String.intern() 则依赖字符串池实现常量共享。（1）String.intern()：字符串驻留// 注册本地方法：java/lang/String.intern() func init() { native.Register("java/lang/String", "intern", "()Ljava/lang/String;", intern) } // intern 实现：将字符串驻留到字符串池 func intern(frame *rtda.Frame) { this := frame.LocalVars().GetThis() // 当前 String 对象 interned := heap.InternString(this) // 从字符串池获取驻留的字符串 frame.OperandStack().PushRef(interned) // 推送结果 } // InternString 实现字符串驻留 func InternString(jStr *Object) *Object { goStr := GoString(jStr) // 从 String 对象获取 Go 字符串 // 检查字符串池，存在则返回，否则添加 if interned, ok := internedStrings[goStr]; ok { return interned } internedStrings[goStr] = jStr return jStr }四、功能测试通过测试案例验证本地方法和反射机制的正确性。1. 反射测试：ClassTest 验证类名获取测试目标：通过反射获取基本类型、数组、普通类的类名。public class ClassTest { public static void main(String[] args) { System.out.println(void.class.getName()); // void System.out.println(boolean.class.getName()); // boolean System.out.println(int[].class.getName()); // [I System.out.println(Object.class.getName()); // java.lang.Object System.out.println("abc".getClass().getName()); // java.lang.String } }测试结果：正确输出各类的规范名称，验证 getClass()、getName0() 等本地方法正常工作。2. 字符串测试：StrTest 验证 intern() 机制测试目标：验证字符串池的驻留机制（intern() 后相同内容字符串引用相同）。public class StrTest { public static void main(String[] args) { String s1 = "abc1"; String s2 = "abc1"; System.out.println(s1 == s2); // true（常量池相同引用） int x = 1; String s3 = "abc" + x; // 动态拼接，初始不在常量池 System.out.println(s1 == s3); // false s3 = s3.intern(); // 驻留到字符串池 System.out.println(s1 == s3); // true（引用相同） } }测试结果：输出符合预期，验证 intern() 方法和字符串池机制正确。本章小结本章实现了本地方法调用和反射机制的核心逻辑，重点包括：本地方法框架：通过注册表（map）管理本地方法，注入伪字节码支持解释器流程，实现 invokenative 指令调用逻辑；反射机制：绑定类对象（java/lang/Class 实例）与类元信息，实现 getClass()、getName0() 等核心反射本地方法；关键本地方法：实现 System.arraycopy()（数组拷贝）、String.intern()（字符串驻留）等类库依赖的本地方法；功能验证：通过反射类名测试和字符串驻留测试，验证本地方法和反射机制的正确性。本地方法和反射是 Java 灵活性的重要支撑，下一章将完善异常处理机制，使 JVM 能更健壮地处理运行时错误。源码地址：https://github.com/Jucunqi/jvmgo.git
自己动手写 Java 虚拟机笔记 - 第八部分：数组与字符串的实现
前言在前一章中，我们实现了方法调用与返回机制，支撑了函数执行的核心流程。本章将聚焦 JVM 中数组和字符串的实现—— 这两类数据结构在 Java 中使用频繁，但它们的创建、存储和操作逻辑与普通对象存在显著差异。数组类由 JVM 运行时动态生成，而非从 Class 文件加载；字符串则通过常量池和字符串池实现共享。本章将详细实现这些特性，完善 JVM 对复杂数据结构的支持。参考资料《自己动手写 Java 虚拟机》—— 张秀宏开发环境工具 / 环境版本说明操作系统MacOS 15.5基于 Intel/Apple Silicon 均可JDK1.8用于字节码分析和测试Go 语言1.23.10项目开发主语言第八章：数组与字符串的核心实现数组和字符串是 Java 中最基础的数据结构，但其底层实现逻辑与普通对象不同。数组类由 JVM 动态生成，支持多维度和多种数据类型；字符串则通过常量池和字符串池实现高效存储和共享。本章将从数据结构设计、指令实现到功能测试，完整覆盖这两类结构的核心机制。一、数组概述：与普通类的本质区别数组是一种特殊的引用类型，其类信息并非来自 Class 文件，而是由 JVM 在运行时动态创建。理解数组与普通类的区别是实现的基础。特性普通类数组类类信息来源从 Class 文件加载由 JVM 运行时动态生成创建指令new 指令 + 构造器初始化newarray/anewarray/multianewarray 指令类名格式全限定名（如 java/lang/String）特殊格式（如 [I 表示 int 数组，[[Ljava/lang/Object; 表示二维对象数组）继承关系显式继承父类隐式继承 java/lang/Object，实现 Cloneable 和 Serializable 接口核心差异：数组类的结构由 JVM 动态定义，无需预编译的 Class 文件；其创建和操作依赖专门的指令，而非普通对象的 new 指令和构造器。二、数组的核心实现1. 数组对象的数据结构数组对象仍复用 Object 结构体，但通过 interface{} 字段存储数组元素（支持不同类型的数组数据）：// Object 统一表示普通对象和数组对象 type Object struct { class *Class // 所属的类（数组类或普通类） data interface{} // 存储数据：普通对象存字段槽位，数组存元素集合 }设计说明：对于普通对象，data 字段存储实例变量的槽位数组（Slots）；对于数组对象，data 字段存储 Go 切片（如 []int32 对应 int 数组，[]*Object 对应对象数组），通过 interface{} 兼容不同类型的数组元素。2. 数组类的动态生成数组类由 JVM 动态创建，无需加载 Class 文件。其类信息（如名称、继承关系）由 JVM 按固定规则生成：// NewArray 创建数组对象（根据数组类和长度初始化元素） func (c *Class) NewArray(count uint) *Object { if !c.IsArray() { panic("Not array class: " + c.name) // 校验是否为数组类 } // 根据数组类名创建对应类型的 Go 切片（映射 Java 数组类型） switch c.Name() { case "[Z": // boolean 数组 return &Object{class: c, data: make([]int8, count)} // boolean 用 int8 存储 case "[B": // byte 数组 return &Object{class: c, data: make([]int8, count)} case "[C": // char 数组 return &Object{class: c, data: make([]uint16, count)} // char 用 uint16 存储 case "[S": // short 数组 return &Object{class: c, data: make([]int16, count)} case "[I": // int 数组 return &Object{class: c, data: make([]int32, count)} case "[J": // long 数组 return &Object{class: c, data: make([]int64, count)} case "[F": // float 数组 return &Object{class: c, data: make([]float32, count)} case "[D": // double 数组 return &Object{class: c, data: make([]float64, count)} default: // 对象数组（如 [Ljava/lang/Object;） return &Object{class: c, data: make([]*Object, count)} } }类型映射规则：Java 数组类型与 Go 切片类型的映射需严格对应，确保元素存储和操作的正确性（如 boolean 数组在 JVM 中实际用 byte 存储，故映射为 []int8）。3. 数组类的加载逻辑数组类的加载由类加载器特殊处理，无需读取 Class 文件，直接动态生成类信息：// LoadClass 加载类（支持普通类和数组类） func (c *ClassLoader) LoadClass(name string) *Class { // 1. 检查缓存，已加载则直接返回 if class, ok := c.classMap[name]; ok { return class } // 2. 若为数组类，动态生成类信息 if name[0] == '[' { return c.loadArrayClass(name) } // 3. 加载普通类（从 Class 文件读取） return c.loadNonArrayClass(name) } // loadArrayClass 动态生成数组类信息 func (c *ClassLoader) loadArrayClass(name string) *Class { // 构建数组类的基本信息 class := &Class{ accessFlags: ACC_PUBLIC, // 数组类默认为 public name: name, // 数组类名（如 "[I"） loader: c, // 类加载器 initStarted: true, // 数组类无需初始化 superClass: c.LoadClass("java/lang/Object"), // 继承 Object interfaces: []*Class{ // 实现 Cloneable 和 Serializable 接口 c.LoadClass("java/lang/Cloneable"), c.LoadClass("java/io/Serializable"), }, } c.classMap[name] = class // 存入缓存 return class }关键逻辑：数组类的继承和接口实现是固定的（继承 Object，实现 Cloneable 和 Serializable），无需像普通类那样从 Class 文件解析。三、数组操作指令实现JVM 提供专门的指令用于数组的创建、长度获取和元素访问，以下是核心指令的实现。1. newarray：创建基本类型数组用于创建基本类型的一维数组（如 int[]、float[]），操作数包括基本类型标识和数组长度。// 基本类型与 atype 对应关系（JVM 规范定义） const ( AT_BOOLEAN = 4 // boolean 数组 AT_CHAR = 5 // char 数组 AT_FLOAT = 6 // float 数组 AT_DOUBLE = 7 // double 数组 AT_BYTE = 8 // byte 数组 AT_SHORT = 9 // short 数组 AT_INT = 10 // int 数组 AT_LONG = 11 // long 数组 ) // NEW_ARRAY 创建基本类型数组 type NEW_ARRAY struct { atype uint8 // 基本类型标识（对应上述常量） } // 从字节码读取 atype 操作数 func (n *NEW_ARRAY) FetchOperands(reader *base.BytecodeReader) { n.atype = reader.ReadUint8() } // 执行指令：创建数组并推送引用到操作数栈 func (n *NEW_ARRAY) Execute(frame *rtda.Frame) { stack := frame.OperandStack() // 1. 从操作数栈弹出数组长度（必须非负） count := stack.PopInt() if count < 0 { panic("java.lang.NegativeArraySizeException") } // 2. 获取类加载器，解析数组类 classLoader := frame.Method().Class().Loader() arrClass := getPrimitiveArrayClass(classLoader, n.atype) // 3. 创建数组对象并推送引用到栈顶 arr := arrClass.NewArray(uint(count)) stack.PushRef(arr) } // 根据 atype 获取对应的数组类 func getPrimitiveArrayClass(loader *heap.ClassLoader, atype uint8) *heap.Class { switch atype { case AT_BOOLEAN: return loader.LoadClass("[Z") // boolean 数组类名为 "[Z" case AT_BYTE: return loader.LoadClass("[B") // byte 数组类名为 "[B" // 省略其他类型映射... default: panic("Invalid atype!") } }执行流程：从操作数栈获取数组长度并校验非负；根据 atype 确定数组类型（如 AT_INT 对应 [I 类）；创建数组对象并将引用推送回操作数栈。2. anewarray：创建引用类型数组用于创建引用类型的一维数组（如 String[]、Object[]），操作数包括类符号引用索引和数组长度。// ANEW_ARRAY 创建引用类型数组 type ANEW_ARRAY struct { base.Index16Instruction // 包含常量池索引（指向类符号引用） } func (a *ANEW_ARRAY) Execute(frame *rtda.Frame) { cp := frame.Method().Class().ConstantPool() // 1. 解析类符号引用，获取元素类型 classRef := cp.GetConstant(a.Index).(*heap.ClassRef) componentClass := classRef.ResolveClass() // 如 "java/lang/String" // 2. 从操作数栈弹出数组长度并校验 stack := frame.OperandStack() count := stack.PopInt() if count < 0 { panic("java.lang.NegativeArraySizeException") } // 3. 获取数组类（元素类型的数组类，如 "[Ljava/lang/String;"） arrClass := componentClass.ArrayClass() // 4. 创建数组对象并推送引用 arr := arrClass.NewArray(uint(count)) stack.PushRef(arr) } // ArrayClass 获取元素类型对应的数组类 func (c *Class) ArrayClass() *Class { arrClassName := "[" + c.name // 数组类名规则：元素类名前加 "[" return c.loader.LoadClass(arrClassName) }关键区别：与 newarray 不同，anewarray 需要先解析类符号引用获取元素类型，再动态生成数组类（如元素类型为 String 时，数组类为 [Ljava/lang/String;）。3. arraylength：获取数组长度用于获取数组的长度，无显式操作数，仅需数组引用。// ARRAY_LENGTH 获取数组长度 type ARRAY_LENGTH struct { base.NoOperandsInstruction } func (a *ARRAY_LENGTH) Execute(frame *rtda.Frame) { stack := frame.OperandStack() // 1. 从栈顶弹出数组引用并校验非空 arrRef := stack.PopRef() if arrRef == nil { panic("java.lang.NullPointerException") } // 2. 获取数组长度并推送回栈顶 length := arrRef.ArrayLength() stack.PushInt(length) } // ArrayLength 计算数组长度（根据数组类型返回对应切片长度） func (o *Object) ArrayLength() int32 { switch o.data.(type) { case []int8: return int32(len(o.data.([]int8))) case []uint16: return int32(len(o.data.([]uint16))) case []int32: return int32(len(o.data.([]int32))) // 省略其他类型... case []*Object: return int32(len(o.data.([]*Object))) default: panic("Not array!") } }实现逻辑：数组长度本质是底层 Go 切片的长度，通过类型断言获取不同切片的长度并返回。4. 数组元素访问指令：<t>aload 和 <t>astore<t>aload：从数组指定索引加载元素到操作数栈（如 iaload 加载 int 元素，aaload 加载引用元素）；<t>astore：将操作数栈顶元素存入数组指定索引（如 iastore 存储 int 元素，aastore 存储引用元素）。以 aaload（引用元素加载）和 iastore（int 元素存储）为例：// AALOAD 从引用数组加载元素 func (a *AALOAD) Execute(frame *rtda.Frame) { stack := frame.OperandStack() // 1. 弹出索引和数组引用 index := stack.PopInt() arrRef := stack.PopRef() // 2. 校验非空和索引越界 checkNotNil(arrRef) refs := arrRef.Refs() // 获取引用数组（[]*Object） checkIndex(len(refs), index) // 3. 推送元素到栈顶 stack.PushRef(refs[index]) } // IASTORE 向 int 数组存储元素 func (i *IASTORE) Execute(frame *rtda.Frame) { stack := frame.OperandStack() // 1. 弹出值、索引和数组引用 val := stack.PopInt() index := stack.PopInt() arrRef := stack.PopRef() // 2. 校验非空和索引越界 checkNotNil(arrRef) ints := arrRef.Ints() // 获取 int 数组（[]int32） checkIndex(len(ints), index) // 3. 存储元素 ints[index] = val } // 辅助函数：校验数组非空 func checkNotNil(ref *heap.Object) { if ref == nil { panic("java.lang.NullPointerException") } } // 辅助函数：校验索引不越界 func checkIndex(arrLen int, index int32) { if index < 0 || index >= int32(arrLen) { panic("java.lang.ArrayIndexOutOfBoundsException") } }通用逻辑：所有元素访问指令均需先校验数组非空和索引合法性，再执行加载或存储操作，区别仅在于元素类型的处理。四、字符串的实现Java 字符串通过 java/lang/String 类表示，其核心是字符数组的封装，且通过字符串池实现常量字符串的共享。1. 字符串的本质：字符数组的封装String 类的核心字段是 value（字符数组，存储字符串内容）和 hash（缓存哈希值），JVM 中通过对象字段模拟这一结构：// Java 中的 String 类简化结构 public final class String { private final char value[]; // 存储字符串内容 private int hash; // 缓存哈希值（默认 0） // ... 构造器和方法 ... }在 JVM 实现中，字符串对象的 data 字段存储字符数组的引用，通过字段访问指令操作 value 数组。2. 字符串池：常量字符串的共享机制为节省内存，JVM 对字符串常量采用 “驻留” 机制 —— 相同内容的字符串常量在字符串池中仅存储一份，通过 intern() 方法实现共享。// 字符串池：key 为 Go 字符串（内容），value 为 Java String 对象 var internedStrings = map[string]*Object{} // JString 将 Go 字符串转换为 Java String 对象（并驻留到字符串池） func JString(loader *ClassLoader, goStr string) *Object { // 1. 检查字符串池，若已存在则直接返回 if internedStr, ok := internedStrings[goStr]; ok { return internedStr } // 2. 将 Go 字符串转换为 char 数组（[]uint16） chars := stringToUtf16(goStr) // 3. 创建 char 数组对象（"[C" 类） jChars := &Object{loader.LoadClass("[C"), chars} // 4. 创建 String 对象（"java/lang/String" 类） jStrClass := loader.LoadClass("java/lang/String") jStr := jStrClass.NewObject() // 5. 为 String 对象的 "value" 字段赋值（字符数组） jStr.SetRefVar("value", "[C", jChars) // 6. 存入字符串池 internedStrings[goStr] = jStr return jStr } // stringToUtf16 将 Go 字符串转换为 UTF-16 编码的 char 数组（[]uint16） func stringToUtf16(s string) []uint16 { runes := []rune(s) // 转换为 Unicode 码点 chars := make([]uint16, len(runes)) for i, r := range runes { chars[i] = uint16(r) } return chars }核心逻辑：字符串池通过 Go map 实现，键为字符串内容，值为对应的 String 对象；当创建字符串时，先检查池中有否相同内容的字符串，若有则复用，否则创建新对象并加入池。五、功能测试1. 数组测试：冒泡排序验证数组指令通过冒泡排序算法验证数组的创建、元素访问和修改指令的正确性：// 测试类：冒泡排序 public class BubbleSortTest { public static void main(String[] args) { int[] arr = {22, 84, 77, 56, 10, 43, 59}; int[] ints = bubbleSort(arr); for (int anInt : ints) { System.out.println(anInt); // 输出排序结果：10 22 43 56 59 77 84 } } public static int[] bubbleSort(int[] arr) { boolean swapped = true; int j = 0; int tmp; while (swapped) { swapped = false; j++; for (int i = 0; i < arr.length - j; i++) { if (arr[i] > arr[i + 1]) { tmp = arr[i]; arr[i] = arr[i + 1]; arr[i + 1] = tmp; swapped = true; } } } return arr; } }测试结果：排序后的数组元素按从小到大输出，验证 newarray、iaload、iastore、arraylength 等指令正常工作。2. 字符串测试：Hello World 验证字符串池通过经典的 Hello World 程序验证字符串创建和输出功能：// 测试类：输出 Hello World public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World"); // 输出字符串 } }测试结果：成功输出 Hello World，验证字符串池、字符数组封装及 println 方法调用的正确性。本章小结本章实现了 JVM 中数组和字符串的核心机制，重点包括：数组的特殊实现：数组类由 JVM 动态生成，通过 interface{} 存储不同类型的数组元素，支持基本类型和引用类型数组；数组指令集：实现 newarray/anewarray（创建数组）、arraylength（获取长度）、<t>aload/<t>astore（元素访问）等指令，覆盖数组操作全流程；字符串机制：通过 java/lang/String 类封装字符数组，利用字符串池实现常量字符串的共享，减少内存占用；功能验证：通过冒泡排序和 Hello World 程序验证数组指令和字符串功能的正确性。数组和字符串的支持是 JVM 功能完整性的重要标志，下一章将讲述本地方法调用与反射的核心机制。源码地址：https://github.com/Jucunqi/jvmgo.git
自己动手写 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
自己动手写 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
自己动手写 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
自己动手写 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
自己动手写 Java 虚拟机笔记 - 第二部分：搜索 Class 文件
前言为什么要自己实现 JVM？兴趣驱动：想深入理解 "Write once, run anywhere" 的底层逻辑，跳出 "API 调用程序员" 的舒适区，亲手剖析 JVM 的核心原理。填补空白：目前网上关于 JVM 实现的资料中，针对 Mac 平台的实践较少，希望通过这份笔记给同类需求的开发者提供参考。为什么选择 Go 语言？开发效率优势：相比 C/C++，Go 语言语法简洁、内存管理更友好，能降低开发门槛，让精力更聚焦于 JVM 核心功能的实现逻辑。学习双赢：借这个项目系统学习 Go 语言，在实践中掌握并发、指针、接口等特性。参考资料《自己动手写 Java 虚拟机》—— 张秀宏（核心参考书籍，推荐对 JVM 实现感兴趣的同学阅读）开发环境工具 / 环境版本说明操作系统MacOS 15.5基于 Intel/Apple Silicon 均可JDK1.8用于字节码分析和测试Go 语言1.23.10项目开发主语言第二章 搜索 Class 文件在 JVM 的执行流程中，第一步就是根据类名找到对应的 Class 文件并读取其内容。本章将实现 Class 文件的搜索逻辑，核心是解析类路径（classpath），并根据路径类型（目录、JAR 包等）加载 Class 文件。一、解析 JRE 路径：获取基础类库位置JVM 运行时依赖 JRE 中的基础类库（如java.lang.Object等核心类），因此需要先确定 JRE 的位置。我们通过命令行参数-Xjre接收用户指定的 JRE 路径，若未指定则自动查找系统默认 JRE。1.1 扩展命令行参数解析修改cmd.go，新增-Xjre参数的解析逻辑，用于接收 JRE 路径：// 命令行选项和参数结构体 type Cmd struct { // ... 省略其他字段 xJreOption string // 存储-Xjre参数的值 } // 解析命令行参数，赋值到Cmd结构体 func parseCmd() *Cmd { cmd := &Cmd{} flag.Usage = printUsage // 解析基础参数 flag.BoolVar(&cmd.helpFlag, "help", false, "print help message") flag.BoolVar(&cmd.helpFlag, "?", false, "print help message") flag.BoolVar(&cmd.versionFlag, "version", false, "print version and exit") flag.StringVar(&cmd.cpOption, "cp", "", "classpath") // 新增：解析-Xjre参数 flag.StringVar(&cmd.xJreOption, "Xjre", "", "path to jre") flag.Parse() // ... 省略后续逻辑 return cmd }说明：-Xjre是 JVM 的非标准选项（以-X开头），用于指定 JRE 的根目录，优先级高于系统默认 JRE 路径。1.2 自动查找 JRE 路径若用户未指定-Xjre，需要自动查找系统默认 JRE。在 MacOS 上，可通过以下逻辑实现（补充getJreDir函数）：// 获取JRE目录（简化版） func getJreDir(jreOption string) string { // 1. 优先使用用户指定的-Xjre参数 if jreOption != "" && exists(jreOption) { return jreOption } // 2. 查找当前Java_home下的JRE if javaHome := os.Getenv("JAVA_HOME"); javaHome != "" { jreDir := filepath.Join(javaHome, "jre") if exists(jreDir) { return jreDir } } // 3. 尝试当前目录下的jre文件夹 if exists("./jre") { return "./jre" } // 4. 若都找不到，抛出错误 panic("can not find jre directory") } // 辅助函数：判断路径是否存在 func exists(path string) bool { _, err := os.Stat(path) return err == nil || os.IsExist(err) }核心逻辑：JRE 路径的查找优先级为 “用户指定> JAVA_HOME 下的 jre > 当前目录 jre”，确保在大多数环境下能正确定位基础类库。二、设计 Entry 接口：统一 Class 文件读取逻辑Class 文件可能存在于目录、JAR 包、ZIP 包等不同位置，为了统一读取逻辑，我们定义Entry接口，抽象不同存储介质的 Class 文件读取行为。2.1 Entry 接口定义const pathListSeparator = string(os.PathListSeparator) // 路径分隔符（Mac/Linux为:，Windows为;） // Entry接口：定义Class文件读取规范 type Entry interface { // 读取类文件：返回类字节码、当前Entry实例、错误信息 readClass(name string) ([]byte, Entry, error) // 字符串表示：返回当前Entry的路径描述 String() string }接口作用：无论 Class 文件在目录还是 JAR 包中，都通过readClass方法读取，调用者无需关心底层存储细节。2.2 Entry 的实现类根据 Class 文件的存储位置，Entry有 4 种实现，覆盖所有常见场景：实现类适用场景示例路径DirEntry目录中的 Class 文件/Users/dev/classesZipEntryJAR/ZIP 包中的 Class 文件/Users/dev/lib/tools.jarWildcardEntry通配符匹配的多个 JAR/ZIP 包/Users/dev/lib/*CompositeEntry多个路径（用分隔符分隔）./classes:/Users/dev/lib/*2.2.1 工厂方法：创建 Entry 实例通过newEntry函数根据路径自动选择合适的实现类，简化调用：// 根据路径创建对应的Entry实例 func newEntry(path string) Entry { // 1. 处理多路径（含分隔符） if strings.Contains(path, pathListSeparator) { return newCompositeEntry(path) } // 2. 处理通配符路径（以*结尾） if strings.HasSuffix(path, "*") { return newWildcardEntry(path) } // 3. 处理JAR/ZIP包 if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") || strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") { return newZipEntry(path) } // 4. 默认视为目录 return newDirEntry(path) }设计思路：通过工厂模式隐藏具体实现类的创建细节，调用者只需传入路径即可获得可用的Entry实例。三、Classpath 类：管理完整类路径JVM 的类路径由三部分组成：启动类路径（boot classpath）、扩展类路径（ext classpath）、用户类路径（user classpath）。Classpath类负责管理这三部分路径的解析和使用。3.1 Classpath 结构定义package classpath import ( "os" "path/filepath" ) // Classpath：管理完整的类路径 type Classpath struct { bootClasspath Entry // 启动类路径（如JRE/lib下的类） extClasspath Entry // 扩展类路径（如JRE/lib/ext下的类） userClasspath Entry // 用户类路径（-cp指定或默认当前目录） }3.2 解析类路径3.2.1 解析启动类和扩展类路径启动类路径包含 JVM 运行必需的核心类（如java.lang包下的类），扩展类路径包含系统扩展类，两者均位于 JRE 目录中：// 解析启动类路径和扩展类路径 func (c *Classpath) parseBootAndExtClasspath(jreOption string) { jreDir := getJreDir(jreOption) // 先获取JRE目录 // 启动类路径：JRE/lib/*（匹配所有JAR包和类目录） jreLibPath := filepath.Join(jreDir, "lib", "*") c.bootClasspath = newWildcardEntry(jreLibPath) // 扩展类路径：JRE/lib/ext/* jreExtPath := filepath.Join(jreDir, "lib", "ext", "*") c.extClasspath = newWildcardEntry(jreExtPath) }说明：JRE/lib/*会匹配该目录下所有 JAR 包（如rt.jar是核心类库），确保能加载基础类。3.2.2 解析用户类路径用户类路径由-cp参数指定（若未指定则默认当前目录），用于加载应用程序自身的类：// 解析用户类路径 func (c *Classpath) parseUserClasspath(cpOption string) { if cpOption == "" { cpOption = "." // 默认当前目录 } c.userClasspath = newEntry(cpOption) }3.3 统一读取 Class 文件Classpath提供ReadClass方法，按优先级（用户类路径 → 扩展类路径 → 启动类路径）查找并读取 Class 文件：// 读取Class文件：按用户类路径→扩展类路径→启动类路径的顺序查找 func (c *Classpath) ReadClass(name string) ([]byte, Entry, error) { name = name + ".class" // 补充.class后缀 // 1. 先从用户类路径查找 if data, entry, err := c.userClasspath.readClass(name); err == nil { return data, entry, nil } // 2. 再从扩展类路径查找 if data, entry, err := c.extClasspath.readClass(name); err == nil { return data, entry, nil } // 3. 最后从启动类路径查找 return c.bootClasspath.readClass(name) }优先级说明：用户类路径优先级最高，避免自定义类覆盖核心类；启动类路径优先级最低，确保核心类不被意外替换。四、测试验证：读取 Class 文件修改main.go的startJVM函数，验证 Classpath 是否能正确读取 Class 文件：func startJVM(cmd *Cmd) { // 解析类路径 cp := classpath.NewClasspath(cmd.xJreOption, cmd.cpOption) fmt.Printf("classpath: %v\n", cp) // 转换类名为路径格式（如java.lang.Object → java/lang/Object） classname := strings.ReplaceAll(cmd.class, ".", "/") // 读取Class文件 data, entry, err := cp.ReadClass(classname) if err != nil { fmt.Printf("Could not find or load main class %s: %v\n", cmd.class, err) return } // 输出类的内容 fmt.Printf("class data:%v\n", data) }测试步骤与结果编译安装：go install ./ch02/执行测试：读取java.lang.Object类（JRE 核心类）ch02 java.lang.String输出结果：结果说明：成功从 JRE 的rt.jar中读取到java.lang.String类的字节码，验证了类路径解析和 Class 文件读取逻辑的正确性。五、小结本章实现了 JVM 搜索 Class 文件的核心逻辑，关键知识点：类路径组成：启动类路径、扩展类路径、用户类路径的分层设计，确保类加载的优先级和安全性。Entry 接口：通过接口抽象不同存储介质的 Class 文件读取行为，简化上层调用。路径解析：支持目录、JAR 包、通配符等多种路径格式，兼容 Java 的类路径规范。下一章将基于本章的 Class 文件读取功能，实现 Class 文件的解析，提取常量池、类信息、方法等关键数据。源码地址：https://github.com/Jucunqi/jvmgo.git
自己动手写 Java 虚拟机笔记 - 第一部分：从零搭建命令行工具
前言为什么要自己实现 JVM？兴趣驱动：想深入理解 "Write once, run anywhere" 的底层逻辑，跳出 "API 调用程序员" 的舒适区，亲手剖析 JVM 的核心原理。填补空白：目前网上关于 JVM 实现的资料中，针对 Mac 平台的实践较少，希望通过这份笔记给同类需求的开发者提供参考。为什么选择 Go 语言？开发效率优势：相比 C/C++，Go 语言语法简洁、内存管理更友好，能降低开发门槛，让精力更聚焦于 JVM 核心功能的实现逻辑。学习双赢：借这个项目系统学习 Go 语言，在实践中掌握并发、指针、接口等特性。参考资料《自己动手写 Java 虚拟机》—— 张秀宏（核心参考书籍，推荐对 JVM 实现感兴趣的同学阅读）开发环境工具 / 环境版本说明操作系统MacOS 15.5基于 Intel/Apple Silicon 均可JDK1.8用于字节码分析和测试Go 语言1.23.10项目开发主语言第一章：实现 JVM 命令行工具命令行工具是 JVM 的入口，负责解析参数、配置运行环境，本章将从零搭建一个基础的命令行参数解析器。一、环境准备1. JDK 1.8 安装与配置从 Oracle 官网 或 AdoptOpenJDK 下载 JDK 1.8 安装包。配置环境变量（以 Mac 为例）：在 ~/.bash_profile 或 ~/.zshrc 中添加：export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_xxx.jdk/Contents/Home export PATH=$JAVA_HOME/bin:$PATH执行 source ~/.zshrc 生效，通过 java -version 验证安装成功。2. Go 1.23.10 安装与配置从 Go 官网 下载对应版本安装包，或通过 Homebrew 安装：brew install go@1.23。配置环境变量：export GOROOT=/usr/local/opt/go@1.23/libexec # 取决于安装路径 export GOPATH=$HOME/go # 自定义工作目录 export PATH=$GOROOT/bin:$GOPATH/bin:$PATH执行 go version 验证安装成功。二、命令行工具核心代码实现1. 定义命令行参数结构（cmd.go）该文件负责解析命令行参数，如类路径、主类名、帮助 / 版本信息等。package main import ( "flag" "fmt" "os" ) // Cmd 存储命令行参数解析结果 type Cmd struct { helpFlag bool // 是否显示帮助信息 versionFlag bool // 是否显示版本信息 cpOption string // 类路径（classpath） class string // 要执行的主类名 args []string // 传递给主类的参数 } // parseCmd 解析命令行参数并返回 Cmd 实例 func parseCmd() *Cmd { cmd := &Cmd{} // 自定义帮助信息打印函数 flag.Usage = printUsage // 绑定命令行选项到 Cmd 字段 flag.BoolVar(&cmd.helpFlag, "help", false, "print help message") flag.BoolVar(&cmd.helpFlag, "?", false, "print help message") // 支持 -? 作为帮助选项 flag.BoolVar(&cmd.versionFlag, "version", false, "print version and exit") flag.BoolVar(&cmd.versionFlag, "v", false, "print version and exit") // 支持 -v 作为版本选项 flag.StringVar(&cmd.cpOption, "cp", "", "classpath") // 类路径选项 // 解析参数 flag.Parse() // 获取非选项参数（主类名和程序参数） args := flag.Args() if len(args) > 0 { cmd.class = args[0] // 第一个参数为主类名 cmd.args = args[1:] // 后续参数传递给主类 } return cmd } // printUsage 打印命令行使用帮助 func printUsage() { fmt.Printf("Usage: %s [-options] class [args...]\n", os.Args[0]) }2. 程序主入口（main.go）主函数根据解析后的参数决定执行逻辑，如打印版本、显示帮助或启动 JVM。package main import "fmt" // main 程序入口函数 func main() { cmd := parseCmd() // 解析命令行参数 // 根据参数执行对应逻辑 if cmd.versionFlag { fmt.Println("version 0.0.1") // 版本信息 } else if cmd.helpFlag || cmd.class == "" { printUsage() // 显示帮助或主类名为空时提示用法 } else { startJVM(cmd) // 启动 JVM（本章仅打印参数，后续章节实现核心逻辑） } } // startJVM 模拟 JVM 启动（本章仅打印参数） func startJVM(cmd *Cmd) { fmt.Printf("classpath: %s\nclass: %s\nargs: %v\n", cmd.cpOption, cmd.class, cmd.args) }三、编译与测试1. 编译代码在项目目录下执行编译命令，生成可执行文件：go install ./ch01 # 假设代码放在 ch01 目录下编译成功后，可执行文件会生成在 $GOPATH/bin 目录下（如 ch01）。2. 测试命令行功能查看版本：ch01 -v # 输出：version 0.0.1查看帮助：ch01 -help # 输出：Usage: ch01 [-options] class [args...]测试参数解析：ch01 -cp ./classes com.example.Main arg1 arg2 # 输出： # classpath: ./classes # class: com.example.Main # args: [arg1 arg2]本章小结本章完成了 JVM 命令行工具的基础实现，核心功能包括：解析命令行选项（-help、-version、-cp 等）；提取主类名和程序参数；提供基础的参数校验和帮助提示。下一章将基于此，实现类路径的查找逻辑和类加载的核心流程，逐步构建完整的 JVM 骨架。源码地址：https://github.com/Jucunqi/jvmgo.git