资讯详情

资讯详情

【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 包结构ch11jvmgo/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 Class14 个字段 │ ├── 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/ 控制指令116 条 │ ├── references/ 引用指令184 条 │ ├── 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/monitorexitsynchronized块的进入/退出需要线程和锁jvmgo 是单线程的第 064 篇讲过编译器会为它生成 catch-allbreakpoint(0xCA)调试器断点保留指令需要调试器基础设施impdep2(0xFF)实现相关保留指令0xFE我们用来做invokenative了那为什么总数是约 220而不是 197205 - 8因为wide指令展开后有多种变体iload/lload/iinc的 wide 版本有些指令的实现按类型分文件统计口径不同加上我们自己加的invokenative0xFE大约 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解释模式HotSpotJIT 后fibonacci(30)数秒~0.5 秒~0.01 秒冒泡排序 16 元素毫秒级微秒级纳秒级启动时间~0.1 秒加载几十个类~0.1 秒加载几百个类—jvmgo 比 HotSpot 慢 10~100 倍都是解释执行的情况下。差距主要来自方法查找是线性的LookupMethodInClass遍历继承链HotSpot 用 vtable 是 O(1)常量池解析结果没有内联到字节码HotSpot 有 constant pool cache rewritingGo 的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_flagsname_indexdescriptor_indexattributes属性表9 种Code / ConstantValue / Exceptions / SourceFile / LineNumberTable / LocalVariableTable / InnerClasses / Synthetic / Deprecated描述符规则B C D F I J S Z基本类型、Lxxx;对象、[数组、Vvoid② 指令集第 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实现CloneableSerializable基本类型类void 8 种无超类无接口⑤ 方法调用第 7、9、10 章5 条 invoke 指令invokestatic/invokespecial/invokevirtual/invokeinterface/invokedynamic静态绑定 vs 动态绑定多态的本质LookupMethodInClass(ref.Class(), name, descriptor)参数传递倒序弹栈栈的 LIFO 从左到右的书写顺序argSlotCountlong/double 占 2实例方法 1this6 条返回指令本地方法注入[0xFE, 返回指令]两字节字节码⑥ 异常处理第 10 章athrow指令异常表startPc/endPc/handlerPc/catchTypecatchType 0表示 catch-allfinally/synchronized栈展开逐帧查找找不到就弹帧StackTraceElementfillInStackTrace4.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-beforeJIT 编译⭐⭐⭐⭐⭐热点探测、中间表示IR、逃逸分析、内联、循环优化、寄存器分配每一项都够写一本书。事实上《深入理解 Java 虚拟机》—— GC 和 JIT 占了半本《The Garbage Collection Handbook》—— 专门讲 GC《Java Concurrency in Practice》—— 专门讲多线程JSR-133Java 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 实现还可以走多远
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →