上一篇【第68篇】删除 hack 代码——JVM 的"成人礼"
下一篇【第70篇】扩展方向——JVM 实现还可以走多远
摘要
70 篇系列走到第 69 篇,是时候回头看看走过的路了。
这一篇: 1. **按四大主题重排 11 章**(比按章节顺序更能看清脉络) 2. **jvmgo 的最终代码统计** 3. **与 HotSpot 的能力对比** 4. **"写一个 JVM 需要知道的所有知识"图谱** 5. **书里坦承的"没讨论的重要内容"** ## 一、按主题重排:11 章的真实脉络 书里的原话: > 第 2、第 3、第 6、第 8 章讨论了**类加载器**。 > 第 4、第 6 章讨论了**运行时数据区**。 > 第 7、第 9、第 10 章讨论了**方法调用**。 > 第 5 章编写了一个简单的**解释器**。 画成图:第1章 命令行工具 ─────────────────────────────┐
│ 骨架
第2章 搜索 class 文件 ──┐ │
第3章 解析 class 文件 ──┤ 类加载器 │
第6章 类加载器 / 方法区 ──┤ (第 6 章横跨三个主题) │
第8章 数组类 / 字符串 ──┘ │
│
第4章 运行时数据区 ──┐ 运行时数据区 │
第6章 方法区 / 常量池 ──┘ │
│
第5章 解释器 + 200 条指令 ──── 指令集与解释器 │
│
第7章 Java 方法调用 ──┐ │
第9章 本地方法调用 ──┤ 方法调用 │
第10章 异常处理 ──┘ │
│
第11章 System 类初始化 ─────────────────────────────┘ 收尾
**关键观察**: **① 第 6 章是"枢纽"** —— 它同时属于三个主题(类加载器、运行时数据区、为方法调用打基础)。这也解释了为什么第 6 章在系列里占了 6 篇(043-048)。 **② 四大主题是"交替推进"的,不是线性的**第2章 能读 class 文件 → 但没地方放
第3章 能解析 class 文件 → 但还是没地方放
第4章 有运行时数据区了 → 但没有类可放
第5章 有解释器了 → 但只能跑单方法,没有类
第6章 类加载器 + 方法区 + 对象 → 三大件就位!
第7章 方法调用 → 真正"活"了
第8章 数组 + 字符串 → 加两个特殊类型
第9章 本地方法 → 接上 JDK 类库
第10章 异常处理 → 能正确处理错误
第11章 System.out → 撕掉最后的 hack
**每一章都在补上一章缺的东西**——这是渐进式开发的典范。 **③ 前 5 章打地基,后 6 章建房子** - 第 1-5 章:命令行、类路径、class 解析、数据区、解释器 —— **这些是"能跑字节码"的最小集** - 第 6-11 章:类、对象、方法调用、数组、字符串、本地方法、异常、System —— **这些是"能跑 Java 程序"的必需集**二、jvmgo 的最终代码统计
2.1 规模
| 指标 | 数量 |
|---|---|
| 总代码量 | 约8000 行Go(不含测试) |
| 章节版本 | ch01 ~ ch11,每个版本可独立编译 |
| Go 包数量 | 约15 个 |
| 实现的指令 | 约 220 条 |
| 实现的本地方法 | 约 30 个 |
| 支持的最大程序 | 能跑HelloWorld、冒泡排序、斐波那契递归、字符串拼接、自动装箱、异常捕获 |
2.2 包结构(ch11)
jvmgo/ch11/ ├── main.go 命令行入口(简化到 15 行) ├── cmd.go 命令行选项解析 ├── jvm.go JVM 结构体 + initVM / execMain ├── interpreter.go 解释器循环 loop() + catchErr │ ├── classpath/ 【第2章】类路径 │ ├── classpath.go Classpath 入口 │ ├── entry*.go DirEntry / ZipEntry / CompositeEntry / WildcardEntry │ ├── classfile/ 【第3章】class 文件解析 │ ├── class_file.go ClassFile 结构 │ ├── constant_pool.go 常量池(18 种常量) │ ├── attribute_info.go 属性表(9 种属性) │ ├── member_info.go 字段表 / 方法表 │ ├── attr_code.go Code 属性(含异常表) │ ├── attr_line_number_table.go 行号表 │ └── ... │ ├── rtda/ 【第4章】运行时数据区 │ ├── thread.go Thread │ ├── jvm_stack.go Stack(链表) │ ├── frame.go Frame │ ├── local_vars.go LocalVars │ ├── operand_stack.go OperandStack │ ├── slot.go Slot │ └── heap/ 【第6章】方法区与堆 │ ├── class.go Class(14 个字段) │ ├── class_member.go ClassMember / Field / Method │ ├── constant_pool.go 运行时常量池 │ ├── cp_*.go 各种符号引用 │ ├── class_loader.go 类加载器 │ ├── object.go Object │ ├── array_object.go 数组对象 │ ├── array_class.go 数组类 │ ├── string_pool.go 字符串池 │ ├── method_lookup.go 方法查找 │ ├── exception_table.go 异常表 │ └── class_name_helper.go 类名/描述符转换 │ ├── instructions/ 【第5章】指令集 │ ├── factory.go 指令工厂(200+ case) │ ├── base/ 4 个抽象指令 + 公共逻辑 │ ├── constants/ 常量指令(21 条) │ ├── loads/ 加载指令(41 条,含 xaload 8 条) │ ├── stores/ 存储指令(41 条,含 xastore 8 条) │ ├── stack/ 栈指令(9 条) │ ├── math/ 数学指令(37 条) │ ├── conversions/ 转换指令(15 条) │ ├── comparisons/ 比较指令(19 条) │ ├── control/ 控制指令(11+6 条) │ ├── references/ 引用指令(18+4 条) │ ├── extended/ 扩展指令(6 条) │ └── reserved/ 保留指令(0xFE invokenative) │ └── native/ 【第9、11章】本地方法 ├── registry.go 注册表 ├── java/lang/ Object / Class / String / System │ Float / Double / Throwable / Math ... ├── java/io/ FileOutputStream / FileInputStream │ FileDescriptor / UnixFileSystem ... └── sun/misc/ VM2.3 指令实现统计
书里给了一个精确的数字:
在 Java 虚拟机规范已经定义的205 条指令中,只剩下8 条还没有实现,分别是:
- 控制指令中的
jsr和ret- 扩展指令中的
jsr_w- 引用类指令中的
invokedynamic、monitorenter和monitorexit- 保留指令中的
breakpoint和impdep2
逐条解释为什么没实现:
| 指令 | 用途 | 为什么不实现 |
|---|---|---|
jsr/jsr_w/ret | 子程序调用(Java 6 前实现 finally 用) | 已废弃,Java 6+ 的 javac 不生成(第 039、063 篇讲过) |
invokedynamic | 动态语言支持、Lambda 表达式 | 需要java.lang.invoke整套基础设施(MethodHandle/CallSite/LambdaMetafactory),工作量大(第 049 篇讲过) |
monitorenter/monitorexit | synchronized块的进入/退出 | 需要线程和锁,jvmgo 是单线程的(第 064 篇讲过编译器会为它生成 catch-all) |
breakpoint(0xCA) | 调试器断点 | 保留指令,需要调试器基础设施 |
impdep2(0xFF) | 实现相关 | 保留指令(0xFE我们用来做invokenative了) |
那为什么总数是"约 220"而不是 197(205 - 8)?
因为:
wide指令展开后有多种变体(iload/lload/iinc的 wide 版本)- 有些指令的实现按类型分文件,统计口径不同
- 加上我们自己加的
invokenative(0xFE)
大约 220 条是包括我们自己扩展的计数。
2.4 各章代码量分布(估算)
第1章 命令行工具 ~200 行 ▓ 第2章 类路径 ~300 行 ▓▓ 第3章 class 文件解析 ~1500 行 ▓▓▓▓▓▓▓▓ 第4章 运行时数据区 ~300 行 ▓▓ 第5章 指令集与解释器 ~2500 行 ▓▓▓▓▓▓▓▓▓▓▓▓▓ 第6章 类和对象 ~800 行 ▓▓▓▓ 第7章 方法调用 ~400 行 ▓▓ 第8章 数组和字符串 ~400 行 ▓▓ 第9章 本地方法 ~300 行 ▓▓ 第10章 异常处理 ~150 行 ▓ 第11章 System 类 ~600 行 ▓▓▓ ───────────────────────────────── 合计 ~7450 行第 3 章和第 5 章占了 53%—— 因为它们是"体力活":一个要把 class 文件的每种常量和属性都解析出来,一个要实现 200 多条指令。
这也揭示了一个事实:写 JVM 的大部分工作不是"设计",而是**“按规范老老实实实现”**。真正有设计含量的是第 6、7 章(类模型、方法调用)。
三、jvmgo vs HotSpot:能力对比
3.1 已支持
| 能力 | jvmgo | HotSpot | 备注 |
|---|---|---|---|
| 类路径搜索 | ✅ | ✅ | jvmgo 支持启动类/扩展类/用户类路径 |
| class 文件解析 | ✅ | ✅ | 完整解析所有常量和属性 |
| class 文件验证 | ❌ | ✅ | jvmgo 跳过(verify()空实现) |
| 类加载(加载/链接/初始化) | ✅ 简化 | ✅ | jvmgo 无双亲委派模型 |
| 运行时常量池 | ✅ | ✅ | |
| 对象创建与字段访问 | ✅ | ✅ | |
| 方法调用与返回 | ✅ | ✅ | |
| 多态分派 | ✅ 线性查找 | ✅ vtable/itable | jvmgo 未优化(第 049 篇讲过) |
| 数组 | ✅ | ✅ | jvmgo 用 Go 切片 |
| 字符串 + 字符串池 | ✅ 简化 | ✅ | jvmgo 的池不回收 |
| 本地方法 | ✅ Go 实现 | ✅ JNI/C++ | 机制完全不同 |
| 异常处理 | ✅ 部分 | ✅ | jvmgo 只支持athrow,指令抛的异常还是 panic |
| 反射 | ✅ 最小集 | ✅ | 只有getClass/getName/ 类对象 |
| 栈轨迹 | ✅ | ✅ |
3.2 未支持
| 能力 | 影响 | 书里说明 |
|---|---|---|
| class 文件验证 | 恶意/错误的字节码能让 JVM 崩溃 | 第 6 章明确跳过 |
| 内存管理和垃圾回收 | 靠 Go 的 GC | “如果本书有机会出第 2 版,希望可以涵盖” |
| 类加载器的委派模型 | 没有双亲委派、没有自定义类加载器 | “没有讨论” |
| 多线程 | 单线程,无锁、无synchronized、无Thread | “没有讨论” |
| JIT 编译器 | 纯解释执行,性能差几个数量级 | “没有讨论” |
| 完整异常处理 | 指令抛的运行时异常 catch 不住 | 第 10 章明确说明,留作练习 |
invokedynamic/ Lambda | 跑不了 Lambda 表达式 | 第 5、11 章说明 |
| 泛型 | 其实是 javac 的事,运行时靠擦除 | 不涉及 |
3.3 性能对比
| 场景 | jvmgo | HotSpot(解释模式) | HotSpot(JIT 后) |
|---|---|---|---|
fibonacci(30) | 数秒 | ~0.5 秒 | ~0.01 秒 |
| 冒泡排序 16 元素 | 毫秒级 | 微秒级 | 纳秒级 |
| 启动时间 | ~0.1 秒(加载几十个类) | ~0.1 秒(加载几百个类) | — |
jvmgo 比 HotSpot 慢 10~100 倍(都是解释执行的情况下)。差距主要来自:
- 方法查找是线性的(
LookupMethodInClass遍历继承链),HotSpot 用 vtable 是 O(1) - 常量池解析结果没有内联到字节码(HotSpot 有 constant pool cache rewriting)
- Go 的
interface{}装箱/拆箱开销(Slot结构体虽小,但频繁断言有成本) - 没有 JIT——这是数量级的差距
但对学习项目来说,这个性能完全够用——能跑HelloWorld、能排序、能递归、能处理异常,就够了。
四、写一个 JVM 需要知道的所有知识
把 70 篇的内容压缩成一张图谱:
4.1 六大板块
┌─────────────────────────────────────────────────────────────┐ │ 写一个 JVM 需要知道的知识 │ └─────────────────────────────────────────────────────────────┘ │ ┌───────────┬───────────┼───────────┬───────────┬──────────┐ ▼ ▼ ▼ ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌─────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ ①class │ │②指令集 │ │③运行时 │ │④类加载 │ │⑤方法 │ │⑥异常 │ │ 文件格式│ │ │ │ 数据区 │ │ │ │ 调用 │ │ 处理 │ └────────┘ └────────┘ └─────────┘ └────────┘ └────────┘ └────────┘ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ▼ 魔数/版本 1字节opcode 线程 类路径 解析符号 athrow 常量池18种 + 操作数 JVM栈 加载 引用 异常表 访问标志 11大类 帧 链接 静态绑定 catchType 类/超类/接口 205条 局部变量表 初始化 动态绑定 栈展开 字段表 栈式架构 操作数栈 懒加载 参数传递 栈轨迹 方法表 类型前缀 方法区 <clinit> 返回值 fillIn 属性表9种 单例优化 堆 数组类 本地方法 Stack (Code/异常表/ 运行时 0xFE指令 Trace 行号表/ 常量池 Element ConstantValue)4.2 每个板块的核心知识点
① class 文件格式(第 3 章)
- 魔数
0xCAFEBABE、主次版本号 - 常量池:18 种常量项(Utf8 / Integer / Float / Long / Double / Class / String / Fieldref / Methodref / InterfaceMethodref / NameAndType / MethodHandle / MethodType / InvokeDynamic …)
- 访问标志:18 个常量,其中 3 个位被复用(
0x0020ACC_SUPER/ACC_SYNCHRONIZED 等) - 字段表 / 方法表:
access_flags+name_index+descriptor_index+attributes - 属性表:9 种(Code / ConstantValue / Exceptions / SourceFile / LineNumberTable / LocalVariableTable / InnerClasses / Synthetic / Deprecated)
- 描述符规则:
B C D F I J S Z(基本类型)、Lxxx;(对象)、[(数组)、V(void)
② 指令集(第 5 章)
- 编码:1 字节 opcode + 可变长操作数
- 11 大类:常量 21、加载 33、存储 33、栈 9、数学 37、转换 15、比较 19、控制 11、引用 18、扩展 6、保留 3
- 助记符首字母 = 数据类型(
iint、llong、ffloat、ddouble、a引用、bbyte、cchar、sshort) - 栈式架构:操作数栈是唯一的数据交换场所
- 4 个抽象指令:
NoOperandsInstruction/BranchInstruction/Index8Instruction/Index16Instruction - 单例优化:无状态指令只创建一次
③ 运行时数据区(第 4、6 章)
- 线程私有:PC 寄存器、JVM 栈、帧、局部变量表、操作数栈
- 线程共享:方法区、堆、运行时常量池
Slot无类型设计(4 字节一个槽,long/double 占两个)- 帧的
nextPC字段(指令跳转的关键)
④ 类加载(第 2、3、6、8 章)
- 类路径:启动类路径(
jre/lib)、扩展类路径(jre/lib/ext)、用户类路径(-cp) - 加载 → 链接(验证/准备/解析)→ 初始化
- 懒解析 + 缓存:符号引用首次使用时才解析,解析结果存起来
<clinit>:静态变量赋值 +static {}块,按源码顺序- 触发时机:
new/getstatic/putstatic/invokestatic/ 反射 - 超类先于子类(靠"后压入的先执行"的栈特性)
- 数组类:运行时生成,超类
Object,实现Cloneable+Serializable - 基本类型类:
void+ 8 种,无超类无接口
⑤ 方法调用(第 7、9、10 章)
- 5 条 invoke 指令:
invokestatic/invokespecial/invokevirtual/invokeinterface/invokedynamic - 静态绑定 vs 动态绑定
- 多态的本质:
LookupMethodInClass(ref.Class(), name, descriptor) - 参数传递:倒序弹栈(栈的 LIFO + 从左到右的书写顺序)
argSlotCount:long/double 占 2,实例方法 +1(this)- 6 条返回指令
- 本地方法:注入
[0xFE, 返回指令]两字节字节码
⑥ 异常处理(第 10 章)
athrow指令- 异常表:
startPc/endPc/handlerPc/catchType catchType == 0表示 catch-all(finally/synchronized)- 栈展开:逐帧查找,找不到就弹帧
StackTraceElement+fillInStackTrace
4.3 贯穿全书的设计哲学
写 70 篇下来,可以总结出 jvmgo 的五个一贯做法:
① 用宿主语言的特性替代 JVM 的运行时机制
| JVM 需要 | jvmgo 用 Go 的 |
|---|---|
| 垃圾回收 | Go GC |
| 数组边界检查 | Go slice(第 054 篇又手写了一遍,为了抛对异常) |
| 哈希表 | Go map |
| 编码转换 | unicode/utf16 |
| 内存拷贝 | copy() |
| 浮点位模式 | math.Float32bits |
| 继承 | struct embedding(第 032 篇) |
| 模块初始化 | init()+import _(第 058 篇) |
② 用"最通用的表示"推迟语义判断
Slot{num, ref}不记录类型 —— 让dup2_x2四合一(第 035 篇)、让传参循环无需区分 long/double(第 050 篇)、让Object.data能容纳 9 种形态(第 053 篇)。
代价:无法做类型校验。收益:代码量大幅减少。
③ 懒加载 + 缓存
符号引用解析、类初始化、方法查找……凡是可以推迟的,都推迟到第一次真正需要时,然后缓存结果。
④ 显式状态标志避免递归
initStarted(防止循环初始化,第 052 篇)ResolvedClass/ResolvedMethod/ResolvedField(缓存解析结果,第 6、7 章)
⑤ 每个 hack 都标注 + 说明退役条件
第 068 篇详细总结过。
五、书里坦承的"没讨论的重要内容"
书里最后一段:
不过遗憾的是,有很多重要的内容没有讨论:class 文件验证、内存管理和垃圾回收、类加载器的委派模型、多线程、JIT,等等。如果本书有机会出第 2 版,希望可以涵盖这些内容。
这五项恰好是 JVM 最核心、最难的部分:
| 主题 | 难度 | 为什么难 |
|---|---|---|
| class 文件验证 | ⭐⭐⭐ | StackMapTable 的类型推导,要保证任意字节码不会破坏类型安全 |
| 垃圾回收 | ⭐⭐⭐⭐⭐ | 对象存活判定、分代、并发标记(CMS/G1/ZGC)、停顿时间控制 |
| 类加载器委派模型 | ⭐⭐ | 双亲委派、OSGi、热部署、类隔离 |
| 多线程 | ⭐⭐⭐⭐ | Java 内存模型(JMM)、锁优化(偏向锁/轻量级锁)、volatile、happens-before |
| JIT 编译 | ⭐⭐⭐⭐⭐ | 热点探测、中间表示(IR)、逃逸分析、内联、循环优化、寄存器分配 |
每一项都够写一本书。事实上:
- 《深入理解 Java 虚拟机》—— GC 和 JIT 占了半本
- 《The Garbage Collection Handbook》—— 专门讲 GC
- 《Java Concurrency in Practice》—— 专门讲多线程
- JSR-133(Java Memory Model)—— 光规范就几十页
所以"自己动手写 Java 虚拟机"的定位很清楚:它是入门书,让你知道 JVM 长什么样、怎么动起来的。真正深入要靠后面的专项学习。
而我们的 70 篇系列,是这个入门过程的详细注解版——每一章拆成 5~7 篇,配上 ASCII 图、对照表、踩坑记录。
本篇小结
全书总结,四个部分:
- 按主题重排 11 章——四大主题(类加载器 / 运行时数据区 / 指令集与解释器 / 方法调用)交替推进,第 6 章是枢纽(同时属于三个主题),前 5 章打地基、后 6 章建房子。
- 代码统计——约 8000 行 Go、15 个包、约 220 条指令(规范 205 条,只差 8 条未实现:
jsr/jsr_w/ret/invokedynamic/monitorenter/monitorexit/breakpoint/impdep2)、约 30 个本地方法。第 3 章(class 解析)和第 5 章(指令集)占了 53%——写 JVM 大部分是体力活。 - 与 HotSpot 对比——已支持:类路径、class 解析、常量池、对象、方法调用、多态(线性查找)、数组、字符串、本地方法、部分异常处理、最小反射、栈轨迹。未支持:class 文件验证、GC、双亲委派、多线程、JIT。性能差 10~100 倍(都是解释执行时),主要因为方法查找是线性的、没有常量池缓存重写、没有 JIT。
- 知识图谱——六大板块:class 文件格式 / 指令集 / 运行时数据区 / 类加载 / 方法调用 / 异常处理,每个板块的核心知识点。以及五个贯穿全书的设计哲学:用宿主语言特性替代 JVM 机制、用通用表示推迟语义判断、懒加载 + 缓存、显式状态标志避免递归、hack 要标注退役条件。
下一篇(第 70 篇,也是本系列的最后一篇):扩展方向。如果要继续往下走,JVM 还能走多远——多线程、GC、JIT、泛型、Lambda、模块化,以及 GraalVM / 虚拟线程这些现代 JVM 的新方向。
上一篇【第68篇】删除 hack 代码——JVM 的"成人礼"
下一篇【第70篇】扩展方向——JVM 实现还可以走多远