# 《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》知识点全解 > AI生成|2026年10月28日|codebuddy|原书:周志明《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》,机械工业出版社,2013-06-01 > **我的阅读**:进度 100%|累计 11小时46分钟|开始 2017-06-05|最近 2022-03-28|读完 2018-08-14 ## 一句话总结 Java 代码从 `.java` 到运行、再到并发执行,中间那台"看不见的机器"做了什么:内存怎么分、垃圾怎么收、类怎么加载、字节码怎么跑、热点代码怎么优化、多线程怎么保证正确。它补的是"能跑"到"跑得稳、跑得快"之间的认知断层。 ## 适合谁 / 前置知识 **适合**:一年以上 Java 经验、写过生产代码的人;要排查 OOM、CPU 飙高、GC 停顿的工程师;准备中高级面试的人。 **不适合**:零基础学语法的人;只想要调参速查表的人——本书是原理书,参数只是副产品。 **前置**:Java 语法与集合、进程/线程与虚拟内存的基本概念、能看懂简单字节码更佳。第 2 版以 JDK 1.7 为蓝本,需自行对照 JDK 8 之后的差异(最典型是永久代改为元空间)。 ## 一、全书地图 五部分是一条递进链:数据放在哪(内存)→ 类怎么进来(加载)→ 指令怎么跑(执行引擎)→ 跑得快不快(编译优化)→ 多个线程一起跑(并发)。 | 部分 | 主题 | 章节 | 回答的问题 | |---|---|---|---| | 一 | 走近 Java | 第 1 章 走近 Java | 技术体系、JVM 家族、为何要懂虚拟机 | | 二 | 自动内存管理 | 第 2 章 内存区域与内存溢出异常;第 3 章 垃圾收集器与内存分配策略;第 4 章 性能监控与故障处理工具;第 5 章 调优案例分析与实战 | 对象住哪、何时回收、用什么工具看、故障怎么解 | | 三 | 执行子系统 | 第 6 章 类文件结构;第 7 章 类加载机制;第 8 章 字节码执行引擎;第 9 章 类加载及执行子系统的案例与实战 | `.class` 长什么样、怎么装进来、怎么执行 | | 四 | 编译与优化 | 第 10 章 早期(编译期)优化;第 11 章 晚期(运行期)优化 | 编译器做了哪些语法糖、JIT 如何改写热点代码 | | 五 | 高效并发 | 第 12 章 Java 内存模型与线程;第 13 章 线程安全与锁优化 | 多线程为何看到错的值、锁开销如何降低 | | 附录 | 手册 | B 字节码指令表;C HotSpot 参数表;D OQL 简介;E JDK 版本轨迹 | 查表用,不必通读 | **逻辑**:第二、三部分是"运行时"的两半(数据区 + 执行体);第四部分是执行引擎的加速器;第五部分建立在第二部分之上——堆线程共享,才有可见性问题。第 5、9 章是案例章,本质是前面原理的练习题。 ## 二、核心知识点 **1. 运行时数据区** 五个区:程序计数器、虚拟机栈、本地方法栈、Java 堆、方法区(含运行时常量池),另有直接内存。前三者线程私有,堆与方法区线程共享;栈存栈帧(`-Xss`),堆存实例(`-Xmx/-Xms`),方法区存类元信息、常量、静态变量(1.7 为永久代,`-XX:PermSize`)。**为什么重要**:所有 OOM 与栈溢出都能落到某个具体的区,定位到区就定了排查方向。**误区**:把"方法区"等同"永久代"——前者是规范概念,后者只是当时的实现;混淆 `StackOverflowError`(深度超限)与 `OutOfMemoryError`(容量不足)。 **2. 对象创建、布局与访问** 堆规整用指针碰撞,不规整用空闲列表;并发安全靠 CAS 重试或 TLAB。布局为对象头(Mark Word + 类型指针)→ 实例数据 → 对齐填充。访问有句柄与直接指针两种,HotSpot 用后者。**为什么重要**:Mark Word 直接编码锁状态,是理解锁升级的前提。**误区**:以为 `new` 一定在堆上(逃逸分析后可能栈上分配);以为对象大小等于字段之和(漏了对象头与对齐)。 **3. 存活判定与引用强度** 从 GC Roots 出发做可达性分析,不可达才可回收;Roots 含栈中局部变量引用、静态属性引用、常量引用、JNI 引用。引用分强、软、弱、虚:软引用做缓存,弱引用配合 `ThreadLocal` 的 key。**为什么重要**:引用计数解决不了循环引用,主流 JVM 都不用,这是"对象为何没被回收"的唯一正确模型。**误区**:以为 `System.gc()` 一定回收(只是建议);以为 `finalize()` 可靠救回对象(只能救一次、时机不定);以为方法区不回收(废弃常量与无用类均可回收)。 **4. 四种收集算法** 标记-清除(简单、有碎片)、复制(无碎片、费空间,适合存活少的新生代,Eden : Survivor = 8 : 1 : 1)、标记-整理(无碎片、移动成本高,适合老年代)、分代收集(按存活周期分区各用其法)。**为什么重要**:所有收集器都是这四者的工程组合,看不懂算法就看不懂取舍。**误区**:以为复制算法浪费一半空间——8:1:1 把浪费压到 10%,代价是老年代要做分配担保。 **5. 收集器谱系与 CMS** 新生代 Serial(Client 场景)、ParNew(常配 CMS)、Parallel Scavenge(吞吐优先);老年代 Serial Old、Parallel Old、CMS、G1。CMS 四步:初始标记(STW)→ 并发标记 → 重新标记(STW)→ 并发清除。G1 按 Region 划分,整体标记-整理、局部复制,可预测停顿。**为什么重要**:选收集器本质是选"吞吐优先"还是"停顿优先",直接决定 SLA 形态。**误区**:以为 CMS 完全不停顿(首尾两步仍 STW);以为"并发"不占资源——抢 CPU 且产生浮动垃圾,跟不上就 Concurrent Mode Failure 退化为全停顿。 **6. 分配与回收策略** Eden 优先分配、大对象直接进老年代、长期存活晋升(默认年龄 15)、动态年龄判定(Survivor 中同龄对象总和过半则大于等于该年龄者直接晋升)、空间分配担保。Minor GC 在新生代,频繁但快;Full GC 在老年代,慢且停顿长。**为什么重要**:这是"服务每隔几分钟抖一下"最常见的解释来源。**误区**:以为对象一定在 Eden 出生(大数组、长字符串直接进老年代,更易触发 Full GC);以为调大新生代总是好事(单次 GC 变长,是权衡)。 **7. 监控与故障工具** 命令行 jps / jstat / jinfo / jmap / jhat / jstack,可视化 JConsole / VisualVM,以及 MAT、BTrace。路径:`jps -l` 找进程 → `jstat -gcutil 1000` 看各区使用率与 GC 次数耗时 → `jmap` 导堆 → MAT 看支配树 → `jstack` 看线程栈;CPU 高先用 `top -Hp` 取线程号,转十六进制回 `jstack` 搜 `nid`。**误区**:生产随意 `jmap -dump` 与 `jhat`(前者可能 STW,后者吃内存且无鉴权);只看 GC 次数不看停顿分布,错过长尾。 **8. Class 文件结构** 魔数 `0xCAFEBABE`、版本号、常量池、访问标志、类/父类/接口索引、字段表、方法表、属性表。常量池存字面量与符号引用;字段方法用描述符表示类型(`I`、`Ljava/lang/String;`、`[I`);指令在 `Code` 属性里。**为什么重要**:"一次编写到处运行"的载体是字节码而非 Java 语言,Kotlin、Scala 共存也靠它;改字节码即可改行为,是 AOP、热部署、Mock 的原理。**误区**:以为 `.class` 存的是 Java 代码——只保留符号引用,无注释,默认无变量名,反编译不可能 100% 还原。 **9. 类加载与双亲委派** 加载 → 验证 → 准备 → 解析 → 初始化。准备阶段赋零值(`static int a = 1` 此时为 0),初始化阶段执行 `` 赋真值;五种主动引用才触发初始化。双亲委派保证 `java.lang.Object` 全局唯一,破坏它的典型场景是 SPI(线程上下文加载器)、Tomcat 应用隔离、OSGi 网状委托。**为什么重要**:加载时机决定静态块何时执行、单例何时创建,是"诡异初始化顺序"的根源。**误区**:以为"类相同"只看类名(实为类加载器 + 全限定名);以为双亲委派是硬约束(它是推荐实现)。 **10. 执行引擎与分派** 栈帧含局部变量表、操作数栈、动态链接、返回地址。解析处理编译期可知、运行期不变的方法(静态、私有、构造器、父类方法);分派处理依赖运行时类型的方法。静态分派看静态类型(重载,编译期定),动态分派看实际类型(重写,查虚方法表)。Java 是静态多分派 + 动态单分派。**为什么重要**:重载与重写的行为差异、`invokedynamic` 的意义都在这里。**误区**:以为重载看运行时类型(传 `null` 时选"最具体"的重载,编译期就定了);以为 `invokevirtual` 每次真去搜方法(走 vtable 索引,接近 O(1))。 **11. 编译期语法糖** 泛型擦除(擦为原生类型并插入强制转换,故不能 `new T[]`、不能 `instanceof` 泛型)、自动装箱拆箱(走 `valueOf()`,`-128~127` 命中缓存,超出则新建,`==` 不可靠)、条件编译(`if (常量)` 恒假分支被消除)、变长参数、增强 for、内部类、枚举。**为什么重要**:看似免费的写法都有运行时代价,理解擦除能解释一批诡异编译错误。**误区**:以为 `Integer` 的 `==` 在区间内永远为真(仅缓存区间内,且依赖实现);以为泛型有运行时类型(`List` 与 `List` 的 `getClass()` 相同)。 **12. 运行期优化与 JIT** 解释器与编译器(C1/C2)并存;方法计数器 + 回边计数器触发即时编译,C1 求编译快、C2 求执行快。代表性优化:方法内联(最重要,是其他优化的前提)、公共子表达式消除、数组边界检查消除、逃逸分析(连带栈上分配、同步消除、标量替换);激进优化失败则逆优化回解释执行。**为什么重要**:Java 的慢启动与"跑一会儿变快"都源于此,预热、编译阈值等经验由此派生。**误区**:以为 JIT 结果稳定(依赖运行时 profile,同一段代码可能编译出不同机器码);以为 `-Xcomp` 更快(牺牲启动且可能编译冷分支)。 **13. Java 内存模型** 主内存 + 线程工作内存,8 种原子操作(lock/unlock/read/load/use/assign/store/write)。volatile 保证可见性、禁止指令重排(内存屏障),但不保证复合操作原子性,`i++` 仍需锁或 `AtomicInteger`。happens-before 的规则:程序次序、监视器锁、volatile 变量、线程启动/终止/中断、对象终结、传递性;`as-if-serial` 只保证单线程结果不变。**为什么重要**:并发的三大根源——原子性、可见性、有序性——只有在这个模型里才能被准确描述。**误区**:以为 volatile 可替代锁(无互斥);以为代码顺序就是执行顺序(无 happens-before 约束即可重排);32 位机上非 volatile 的 `long/double` 可能读到半个值。 **14. 线程安全与锁优化** 五个层次:不可变、绝对线程安全、相对、线程兼容、线程对立。优先 `synchronized`(内置、自动释放);需要可中断、超时、公平、多条件变量才用 `ReentrantLock`;非阻塞用 CAS,注意 ABA。能不改共享状态就不改:不可变对象、可重入代码、`ThreadLocal`。锁优化四板斧:自旋与自适应自旋、锁消除、锁粗化、轻量级锁与偏向锁。**为什么重要**:这是同时拿到"正确"与"快"的关键。**误区**:以为 `synchronized` 一定比 `ReentrantLock` 慢(JDK 6 后已大幅优化);以为用了 `ConcurrentHashMap` 就安全(复合操作仍需原子性);以为锁越细越好(更多竞争与开销)。 ## 三、关键概念速查表 | 概念 | 一句话解释 | 用在哪儿 | |---|---|---| | 程序计数器 | 当前线程字节码行号,唯一不会 OOM 的区 | 线程切换后恢复位置 | | 方法区/永久代/元空间 | 类元信息、常量、静态变量的存放地 | 类加载、反射、动态代理 | | 直接内存 | 堆外由 NIO 分配的 Native 内存 | `DirectByteBuffer`、Netty | | GC Roots | 可达性分析的起点集合 | 判断对象是否可回收 | | 空间分配担保 | Minor GC 前确认老年代接得住晋升对象 | 避免晋升失败触发 Full GC | | TLAB | 线程本地分配缓冲 | 高并发下 `new` 的锁竞争 | | 双亲委派 | 先委托父加载器,加载不到再自己来 | 类隔离、SPI、容器 | | 语法糖 | 编译期被改写的便捷写法 | 泛型擦除、装箱、增强 for | | 逃逸分析 | 判断对象是否逃出方法或线程 | 栈上分配、锁消除、标量替换 | | happens-before | 定义"前一个操作对后一个可见"的偏序 | 并发正确性推理 | | CAS | 比较并交换的原子指令 | 原子类、无锁队列 | | 偏向锁 | 无竞争时把锁偏向首个线程,连 CAS 都省 | 单线程反复进同步块 | ## 四、可复用的方法/框架 **A. 线上故障定位四步**:定性(CPU / 内存 / 延迟)→ 定位区(`jstat -gcutil` 看是新生代频繁还是老年代涨满)→ 取证(`jstack` 隔数秒抓三次对比,评估 `jmap` 的 STW 风险)→ 验证(预发复现,一次只改一个变量)。 **B. GC 调优判断清单**:先问目标,吞吐优先还是停顿优先,两者不可兼得;再看分布,Minor GC 太多说明新生代小或对象产生太快,老年代涨满说明泄漏、大对象或晋升过快;三个主旋钮是堆大小(`-Xmx` 与 `-Xms` 设相等)、新生代比例、收集器选择;最后才动 `MaxTenuringThreshold` 一类细参数。 **C. 线程安全三问**:有无共享可变状态?状态是否不可变?所有访问是否在同一把锁或同一原子变量下?三问有一否,就有风险。 **D. 读字节码的路径**:`javap -c -p` 看指令 → 找 `invoke*` 判断调用类型 → 看 `checkcast`、`valueOf` 找语法糖痕迹 → 回常量池核对符号引用。 ## 五、我的阅读轨迹 | 项目 | 数据 | |---|---| | 进度 | 100% | | 累计阅读 | 11小时46分钟 | | 开始阅读 | 2017-06-05 | | 最近阅读 | 2022-03-28 | | 读完时间 | 2018-08-14 | **复习建议**:已读完,首读(2018-08)距最近翻阅(2022-03)近四年,属"读过但细节模糊",重点应放在易忘且高价值处,不必从头再读。第一优先级(每半年):第 2、3 章与第 13 章,排障与面试的直接命中区。第二优先级(每年):第 7、8 章,概念密度最高、最易记混(双亲委派的三次破坏、静态分派与动态分派)。第三优先级(按需):第 4、5 章当工具手册,第 10、11 章遇到"为何这么写更快"时回看,附录 B/C 只查表。需自行补课:元空间、G1 默认化、ZGC 等内容本书缺失或仅提及。 ## 六、行动清单 1. 用 `jps` + `jstat -gcutil` 观察本地服务 5 分钟,记录 GC 次数与停顿,建立基线。 2. 写一个不断向 `List` 添加对象的 demo,用 `-Xmx64m` 触发 OOM,导出 hprof 并在 MAT 中找到支配对象。 3. 对含 `Integer` 比较、泛型集合、增强 for 的代码执行 `javap -c -p`,逐行标注语法糖被改写成什么。 4. 写一个 `volatile` 可见性实验,去掉 volatile 观察死循环,亲手验证 JMM。 5. 用两个线程交叉持锁制造死锁,`jstack` 抓取并读出 `Found one Java-level deadlock`。 6. 整理个人参数卡片:`-Xms/-Xmx/-Xmn/-Xss/-XX:SurvivorRatio/-XX:+UseG1GC`,写明每个参数改变什么、风险是什么。 7. 对照 JDK 8+ 文档,列出本书已过时的结论(永久代、CMS 地位、G1 默认化)并标注。 8. 复述一个真实案例的"现象 → 假设 → 取证 → 结论"链路,写成 300 字小结。 ## 七、延伸阅读 1. **《实战Java虚拟机:JVM故障诊断与性能优化》(葛一鸣)** — 本书的动手版,把第 3–5 章的原理变成可执行实验。**紧接本书读**,把原理转成手感。 2. **《Java并发编程实战》(Brian Goetz 等)** — 本书第 12、13 章讲"虚拟机怎么保证",它讲"程序员怎么设计"。**第二本读**,有 JMM 与锁优化的底子再读 j.u.c 才有代入感。 3. **《Java性能权威指南》(Scott Oaks)** — 覆盖 JDK 8 之后的 G1、JIT 编译日志与堆分析。**最后读**,补上本书的年代空白,形成"原理 → 并发设计 → 现代调优"的闭环。