news 2026/10/14 7:10:05

【DIY系列:Java虚拟机】第69篇:系列总结

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【DIY系列:Java虚拟机】第69篇:系列总结

上一篇【第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/ VM

2.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/monitorexitsynchronized块的进入/退出需要线程和锁,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 已支持

能力jvmgoHotSpot备注
类路径搜索✅✅jvmgo 支持启动类/扩展类/用户类路径
class 文件解析✅✅完整解析所有常量和属性
class 文件验证❌✅jvmgo 跳过(verify()空实现)
类加载(加载/链接/初始化)✅ 简化✅jvmgo 无双亲委派模型
运行时常量池✅✅
对象创建与字段访问✅✅
方法调用与返回✅✅
多态分派✅ 线性查找✅ vtable/itablejvmgo 未优化(第 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 性能对比

场景jvmgoHotSpot(解释模式)HotSpot(JIT 后)
fibonacci(30)数秒~0.5 秒~0.01 秒
冒泡排序 16 元素毫秒级微秒级纳秒级
启动时间~0.1 秒(加载几十个类)~0.1 秒(加载几百个类)—

jvmgo 比 HotSpot 慢 10~100 倍(都是解释执行的情况下)。差距主要来自:

  1. 方法查找是线性的(LookupMethodInClass遍历继承链),HotSpot 用 vtable 是 O(1)
  2. 常量池解析结果没有内联到字节码(HotSpot 有 constant pool cache rewriting)
  3. Go 的interface{}装箱/拆箱开销(Slot结构体虽小,但频繁断言有成本)
  4. 没有 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 图、对照表、踩坑记录。

本篇小结

全书总结,四个部分:

  1. 按主题重排 11 章——四大主题(类加载器 / 运行时数据区 / 指令集与解释器 / 方法调用)交替推进,第 6 章是枢纽(同时属于三个主题),前 5 章打地基、后 6 章建房子。
  2. 代码统计——约 8000 行 Go、15 个包、约 220 条指令(规范 205 条,只差 8 条未实现:jsr/jsr_w/ret/invokedynamic/monitorenter/monitorexit/breakpoint/impdep2)、约 30 个本地方法。第 3 章(class 解析)和第 5 章(指令集)占了 53%——写 JVM 大部分是体力活。
  3. 与 HotSpot 对比——已支持:类路径、class 解析、常量池、对象、方法调用、多态(线性查找)、数组、字符串、本地方法、部分异常处理、最小反射、栈轨迹。未支持:class 文件验证、GC、双亲委派、多线程、JIT。性能差 10~100 倍(都是解释执行时),主要因为方法查找是线性的、没有常量池缓存重写、没有 JIT。
  4. 知识图谱——六大板块:class 文件格式 / 指令集 / 运行时数据区 / 类加载 / 方法调用 / 异常处理,每个板块的核心知识点。以及五个贯穿全书的设计哲学:用宿主语言特性替代 JVM 机制、用通用表示推迟语义判断、懒加载 + 缓存、显式状态标志避免递归、hack 要标注退役条件。

下一篇(第 70 篇,也是本系列的最后一篇):扩展方向。如果要继续往下走,JVM 还能走多远——多线程、GC、JIT、泛型、Lambda、模块化,以及 GraalVM / 虚拟线程这些现代 JVM 的新方向。


上一篇【第68篇】删除 hack 代码——JVM 的"成人礼"
下一篇【第70篇】扩展方向——JVM 实现还可以走多远


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 7:09:49

PMP备考90天:从背诵到项目思维的高效通关手册

走出考场的那一刻&#xff0c;很多人的第一反应往往不是“终于解放了”&#xff0c;而是“那道题我当时为什么要那样选”。我这些年跟很多刚考完PMP的人聊过&#xff0c;几乎每个人都会经历一段“考完才想明白”的瞬间&#xff1a;明明复习时把敏捷原则背得滚瓜烂熟&#xff0c…

作者头像 李华
网站建设 2026/10/14 7:09:44

context-mode:基于SQLite+FTS5+BM25的轻量级上下文增强实践

1. “context-mode”到底是什么&#xff1f;别被术语唬住&#xff0c;它本质是让AI真正“听懂上下文”的工程实践最近在多个技术社区和开发群聊里&#xff0c;“context-mode”这个词突然高频出现&#xff0c;尤其和MCP、SQLite、FTS5、BM25这些词绑在一起刷屏。很多人第一反应…

作者头像 李华
网站建设 2026/10/14 7:09:01

OpenCV图像旋转绕任意中心与缩放:C++实现完整原理与代码

做图像处理的人迟早都会碰到这个需求&#xff1a;把一张图绕某个点转个角度&#xff0c;可能还要顺手缩放下。OpenCV 里warpAffine配合getRotationMatrix2D确实能旋转&#xff0c;但新手用下来基本都会发现两个恼人的问题——默认绕图像中心转&#xff0c;而且输出图像尺寸不变…

作者头像 李华
网站建设 2026/10/14 7:08:57

Android双菜单问题排查与修复:从成因到实战方案

先说说我最近碰到的一件怪事&#xff1a;一个已经跑了好几个版本的APK&#xff0c;在测试机上突然被反馈“菜单有两层”&#xff0c;底部导航上面浮着一个Tab栏&#xff0c;点击“设置”之后右上角又弹出一个原生菜单&#xff0c;里面四个按钮的功能有两项是重复的。用户把截图…

作者头像 李华
网站建设 2026/10/14 7:08:53

蓝桥杯Java A组省赛前三题复盘:稳拿基础分的关键策略

1. 赛题定位与做题策略&#xff1a;前三题决定省奖下限每年蓝桥杯省赛&#xff0c;Java A组前三道题&#xff08;Q1~Q3&#xff09;的定位都非常明确&#xff1a;保底题。它们不追求极致的算法深度&#xff0c;不考冷门数据结构&#xff0c;更多是检验参赛者的基本功——能不能…

作者头像 李华
网站建设 2026/10/14 7:08:35

Mac配置C++开发环境:Clang、LLDB与CMake工具链一致性实战

1. 为什么Mac上配C环境比Windows更“安静”却更费神&#xff1f;很多人第一次在Mac上打开VSCode&#xff0c;敲下#include <iostream>&#xff0c;按下CmdShiftB&#xff0c;结果弹出“无法找到构建任务”——不是报错&#xff0c;是彻底没反应。这种“静音式失败”恰恰是…

作者头像 李华