第 3 章 · 语言运行时与执行引擎
“学 JVM 不是为了写 Java,是为了理解’运行时’这件事本身。“
本章覆盖的课程
| 编号 | 课程 | 本章的处理方式 |
|---|---|---|
| 030 | 深入拆解 Java 虚拟机 | 对照系:GC 算法、内存模型、类加载 → 对标 Go runtime |
| 080 | 图解 Google V8 | 执行引擎样本:隐藏类、内联缓存、JIT → 对标 Go 的编译优化 |
| 058 | JavaScript 核心原理解析 | 语言设计样本:原型、闭包、异步 → 对标 Go 的接口与并发 |
| 010 | 重学前端 | 体系化样本:前端知识地图的构建方法 |
| 142 | 浏览器工作原理与实践 | 系统样本:渲染流水线、事件循环 → 对标 Go 的 GMP |
为什么 Go 程序员要学这些
这 5 门课在教什么
不是教 Java 语法,也不是教前端。它们教的是四个通用的系统设计问题:
① 内存怎么自动回收? → JVM 的 GC / V8 的 GC / Go 的 GC
② 代码怎么执行? → JVM 的解释器+JIT / V8 的 JIT / Go 的 AOT
③ 动态性怎么实现? → JVM 的类加载 / V8 的隐藏类 / Go 的 interface
④ 并发怎么组织? → JVM 的线程 / V8 的事件循环 / Go 的 GMP
这四个问题,任何一个”带 runtime 的语言”都要回答。Go 只是给出了它自己的答案。
对照学习的价值
你只知道一种做法时,你不知道它是好是坏。 你知道三种做法,并且知道它们各自的取舍,你才算懂了。
| 问题 | JVM 的答案 | V8 的答案 | Go 的答案 |
|---|---|---|---|
| 内存回收 | 分代 + 多种 GC 可选 | 分代 + 增量标记 | 不分代 + 并发三色标记 |
| 代码执行 | 字节码 + 解释 + JIT | 字节码 + 解释 + 多层 JIT | 直接编译成机器码 |
| 动态分发 | 虚方法表 (vtable) | 隐藏类 + 内联缓存 | itab(接口表) |
| 并发 | 1:1 线程 | 单线程事件循环 | M:N goroutine |
| 启动速度 | 慢(JVM 启动 + 类加载) | 快(但要 JIT 预热) | 最快(直接是机器码) |
| 峰值性能 | 最高(JIT 能优化) | 高 | 中(AOT 无运行时信息) |
看完这张表,你就明白了:Go 的每一个选择都是”用峰值性能换启动速度和确定性”。
章节结构
| 文档 | 内容 | 动手项目 |
|---|---|---|
00-设计推演:从零造一个 GC.md | ★ 必读开篇:引用计数 → 标记清除 → 标记整理 → 复制式 → 分代 → 并发三色 → 写屏障 → 混合屏障,八次失败演进 | — |
01-运行时解剖:JVM·V8·Go 三方对照.md | 内存布局、对象模型、执行方式、并发模型 | — |
02-动手:用 Go 写一个 GC.md | 标记清除、三色标记、写屏障、并发 GC | 完整可运行的 GC |
03-动手:字节码虚拟机与执行引擎.md | 栈式 VM、寄存器 VM、JIT 原理、逃逸分析 | 字节码 VM + 逃逸分析器 |
★ 先读
00-设计推演。 它回答上面那个”贯穿全章的问题”的方式不是给结论, 而是让你自己走一遍:引用计数为什么栽在循环引用上、标记清除为什么停顿长、 漏标到底需要”哪两个条件同时满足”、以及”栈上加不了屏障”这个约束是怎么把 Go 逼到”必须混合屏障”这条唯一路上的。
学完这一章,你应该能
- 画出 JVM、V8、Go 三者堆内存布局的对比图,解释每种布局的设计动机
- 用 Go 实现一个可用的 GC(标记清除 + 三色标记 + 混合写屏障)
- 解释为什么 Go 的 GC 不需要分代,以及这个选择的代价
- 用 Go 实现一个字节码虚拟机,能执行一个简单的脚本语言
- 解释隐藏类(Hidden Class)和内联缓存(Inline Cache)的原理,并说明 Go 用什么方式解决同样的问题
- 解释浏览器的事件循环和 Go 的 GMP 调度器,为什么是”两种并发哲学”
- 回答这个问题:如果 Go 的 GC 停顿还是太长,给我六个月,我怎么改?
一个贯穿全章的问题
为什么 Go 的 GC 不做分代?
这是理解 Go runtime 设计哲学的钥匙。
标准答案(Java 视角):
分代假说(Weak Generational Hypothesis):大多数对象朝生夕死。所以把堆分成新生代和老年代,新生代用复制算法(只复制存活的少数),老年代用标记清除。
Go 的答案:
Go 没有分代,因为 Go 大量使用栈分配和值类型,堆上的对象大部分是长期存活的,分代假说在 Go 里不成立。
但这个答案引出了更多问题:
- Go 为什么能大量用栈分配?(逃逸分析)
- Go 的写屏障为什么必须是”混合”的?(因为不分代,没有 Remembered Set)
- Go 的 GC 目标为什么是 1ms 而不是吞吐率?(因为 Go 主要用于在线服务)
这一章就是要让你能回答这些”为什么”。
开始阅读:00-设计推演:从零造一个 GC