Lucaju 的技术笔记 - JVM
type: channel
link: https://lucaju.cn/index.php/tag/Jim/
items: 自己动手写 Java 虚拟机笔记 - 第十部分：异常处理机制实现（系列终章）
items: 自己动手写 Java 虚拟机笔记 - 第九部分：本地方法调用与反射机制实现
items: 自己动手写 Java 虚拟机笔记 - 第八部分：数组与字符串的实现
items: 自己动手写 Java 虚拟机笔记 - 第七部分：方法调用与返回机制
items: 自己动手写 Java 虚拟机笔记 - 第五部分：指令集与解释器实现
items: 自己动手写 Java 虚拟机笔记 - 第四部分：实现运行时数据区
items: 自己动手写 Java 虚拟机笔记 - 第三部分：解析 Class 文件核心结构
items: 自己动手写 Java 虚拟机笔记 - 第二部分：搜索 Class 文件
items: 自己动手写 Java 虚拟机笔记 - 第一部分：从零搭建命令行工具

自己动手写 Java 虚拟机笔记 - 第十部分：异常处理机制实现（系列终章）
type: item
date: 2025-07-07T13:36:00+08:00
description: 前言在前一章中，我们实现了本地方法调用与反射机制，让 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 []*ExceptionH…

自己动手写 Java 虚拟机笔记 - 第九部分：本地方法调用与反射机制实现
type: item
date: 2025-07-04T13:50:00+08:00
description: 前言在前一章中，我们实现了数组和字符串的核心机制，完善了 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.co…

自己动手写 Java 虚拟机笔记 - 第八部分：数组与字符串的实现
type: item
date: 2025-07-01T13:22:00+08:00
description: 前言在前一章中，我们实现了方法调用与返回机制，支撑了函数执行的核心流程。本章将聚焦 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 "[…

自己动手写 Java 虚拟机笔记 - 第七部分：方法调用与返回机制
type: item
date: 2025-06-30T13:17:00+08:00
description: 前言在前一章中，我们实现了类与对象的核心机制，包括类信息存储、对象创建和字段访问等。本章将聚焦 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. 方法查找逻辑方法查找需在类本身、父类及实现的接口中递归匹配，确保找到正确的方法：// look…

自己动手写 Java 虚拟机笔记 - 第五部分：指令集与解释器实现
type: item
date: 2025-06-20T13:04:00+08:00
description: 前言在前一章中，我们实现了 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.Operand…

自己动手写 Java 虚拟机笔记 - 第四部分：实现运行时数据区
type: item
date: 2025-06-16T11:33:00+08:00
description: 前言在前一章中，我们完成了 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 …

自己动手写 Java 虚拟机笔记 - 第三部分：解析 Class 文件核心结构
type: item
date: 2025-06-15T11:20:00+08:00
description: 前言在前两章中，我们搭建了 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 } …

自己动手写 Java 虚拟机笔记 - 第二部分：搜索 Class 文件
type: item
date: 2025-06-13T10:59:00+08:00
description: 前言为什么要自己实现 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”，确保在大多数环境下能正确定位基础类库。二、设计 …

自己动手写 Java 虚拟机笔记 - 第一部分：从零搭建命令行工具
type: item
date: 2025-06-13T10:34:00+08:00
description: 前言为什么要自己实现 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] cla…