# 《深入理解Java虚拟机:JVM高级特性与最佳实践》知识点全解 > AI生成|2026年10月28日|codebuddy|原书:周志明《深入理解Java虚拟机:JVM高级特性与最佳实践》,机械工业出版社,2011-07-01 > **我的阅读**:进度 100%|累计 8小时31分钟|开始 2019-03-10|最近 2021-04-21|读完 2019-03-30 ## 一句话总结 Java 代码从 `.java` 到跑起来、再到多线程并发执行,中间那台"看不见的机器"做了什么:内存怎么划分、垃圾怎么回收、类怎么加载、字节码怎么执行、热点代码怎么优化、并发正确性靠什么保证。它补的是"会写 Java"到"知道 Java 为什么这样跑"之间的认知断层。 ## 适合谁 / 前置知识 **适合**:一年以上 Java 经验、写过并部署过真实应用的人;正在排查 OOM、CPU 飙高、GC 停顿的工程师;准备中高级面试、需要把零散知识点串成体系的人。 **不适合**:零基础学语法的人;只想抄参数表的人——本书是原理书,参数只是原理的副产品。 **前置**:Java 语法与集合框架;进程/线程、虚拟内存、栈与堆的基本概念;能看懂简单汇编或字节码更佳。 **时代边界(重要)**:本书 2011 年成书,以 JDK 1.6/1.7 为背景,方法区实现写作"永久代",收集器以 CMS 为主角、G1 尚属试验。读到时须自行对照今日实现:JDK 8 起永久代改为元空间(Metaspace),G1 自 JDK 9 起成为默认收集器。凡与现状冲突处,以官方文档为准,本书价值在原理而非参数。 ## 一、全书地图 五部分是一条单向递进链:**数据放在哪(内存)→ 类怎么进来(加载)→ 指令怎么跑(执行引擎)→ 跑得快不快(编译优化)→ 多个线程一起跑(并发)**。 | 部分 | 目录给出的条目 | 回答的问题 | |---|---|---| | 第一部分 走近 Java | "第一部分 走近 Java"(章号目录未单列,待核对) | Java 技术体系是什么、为什么值得懂虚拟机 | | 第二部分 自动内存管理机制 | 第 3 章 垃圾收集器与内存分配策略;第 4 章 虚拟机性能监控与故障处理工具;第 5 章 调优案例分析与实战 | 对象住哪儿、谁被回收、用什么工具看、线上故障怎么解 | | 第三部分 虚拟机执行子系统 | 第 7 章 虚拟机类加载机制;第 8 章 虚拟机字节码执行引擎;第 9 章 类加载及执行子系统的案例与实战 | `.class` 长什么样、类怎么被装进来、字节码怎么被执行 | | 第四部分 程序编译与代码优化 | 第 11 章 晚期(运行期)优化 | 编译器做了哪些手脚、JIT 如何改写热点代码 | | 第五部分 高效并发 | 第 13 章 线程安全与锁优化 | 多线程为何会看到错的值、锁的开销如何降低 | | 附录 | A 虚拟机家族;B 字节码指令表;C HotSpot 主要参数表;D OQL 简介;E JDK 历史版本轨迹 | 查表与追溯,不必通读 | > 说明:目录中每个部分只单列了部分章节,第二部分的"内存区域与内存溢出异常"、第三部分的"类文件结构"、第四部分的"编译期优化"、第五部分的"Java 内存模型与线程"未单独列出章号,按主题归入对应部分,章号待核对,本文不臆造编号。 **逻辑关系**:第二部分是"数据面",第三部分是"执行面",两者构成本书主干;第四部分是执行引擎的加速器(解释器之外的 JIT);第五部分建立在第二部分之上——正因为堆是线程共享的,才存在可见性与有序性问题。第 5、9 两章是案例章,本质是把前面的原理做成习题。 ## 二、核心知识点 **1. 运行时数据区** 五个区:程序计数器、虚拟机栈、本地方法栈(线程私有),Java 堆、方法区(线程共享),另加直接内存(NIO 的 DirectByteBuffer)。栈帧存局部变量表、操作数栈、动态链接、返回地址;方法区存类元信息、运行时常量池、静态变量,在本书语境下由永久代实现(`-XX:PermSize`)。 **为什么重要**:任何 OOM 与栈溢出都能落到某个具体区域,定区即定排查方向——堆溢出看对象与引用链,栈溢出看递归深度与 `-Xss`,方法区溢出看动态生成类与 `intern`。 **误区**:把方法区等同于永久代(前者是规范概念,后者只是当时的实现);混淆 `StackOverflowError`(深度超限)与 `OutOfMemoryError`(容量不足)。 **2. 对象的一生:创建、布局、定位** 创建:类加载检查 → 分配内存(堆规整用指针碰撞,不规整用空闲列表)→ 内存置零 → 设置对象头 → 执行构造方法。并发分配靠 CAS 重试或 TLAB 规避竞争。布局:对象头(Mark Word + 类型指针)→ 实例数据 → 对齐填充。定位:句柄(引用稳定,多一次间接寻址)或直接指针(更快,HotSpot 采用)。 **为什么重要**:Mark Word 里编码了 hash、GC 年龄、锁标志位,是后面锁升级定理的物质基础。 **误区**:以为 `new` 一定在堆上(逃逸分析后可能栈上分配、标量替换);以为对象大小等于字段之和(漏了对象头与对齐填充)。 **3. OOM 与内存泄漏的分野** 常见形态:`Java heap space`(对象太多/太大/泄漏)、`PermGen space`(1.6 时代动态代理、JSP 编译、`String.intern`)、`unable to create new native thread`(栈总量吃光物理内存)、`Direct buffer memory`。泄漏是对象被无意义地强引用住,溢出是真的装不下,两者处置方向相反。 **怎么用**:先看异常信息定位区域,再用 `-XX:+HeapDumpOnOutOfMemoryError` 留现场,MAT 沿 GC Roots 找"谁抓住了它";确属容量不足才调 `-Xms/-Xmx`。 **误区**:见 OOM 就调大堆(只把爆炸推迟);把 GC 频繁当成泄漏(可能只是缓存没上限或新生代过小)。 **4. 存活判定与四种引用** 可达性分析:从 GC Roots(栈中局部变量、静态属性、常量、JNI 引用、活跃线程)出发,不可达才可回收。引用计数解决不了循环引用,主流 JVM 不用它。引用强度:强(永不回收)、软(内存不足才回收,适合做缓存)、弱(下次 GC 必回收,`ThreadLocal` 的 key 就用它)、虚(回收时收到通知,配合 ReferenceQueue)。 **为什么重要**:这是回答"我的对象为什么没被回收 / 为什么被回收了"的唯一正确框架。 **误区**:以为 `System.gc()` 一定执行(只是建议);以为 `finalize()` 能救回对象(仅一次机会、时机不定、不保证执行);以为方法区不回收(废弃常量与无用类都可回收)。 **5. 四种收集算法与分代思想** 标记-清除(简单、碎片多)、复制(无碎片、费空间;新生代 Eden : Survivor = 8 : 1 : 1,靠分配担保兜底)、标记-整理(无碎片、移动成本高,适合老年代)、分代收集(按存活周期分区,各用其法)。 **为什么重要**:所有收集器都是这四者的工程组合,不懂算法就看不懂收集器的取舍。 **误区**:以为复制算法浪费一半空间(8:1:1 只浪费 10%,代价是老年代要做担保);以为分代是规范强制(是经验总结,G1 之后代际边界已模糊)。 **6. 收集器谱系与 CMS 的四阶段** 新生代:Serial(Client、单核)、ParNew(CMS 的常见搭档)、Parallel Scavenge(吞吐优先,关注 `MaxGCPauseMillis` 与 `GCTimeRatio`);老年代:Serial Old、Parallel Old、CMS;G1 按 Region 划分、整体标记-整理局部复制、可预测停顿(本书写作时仍属试验性)。CMS:初始标记(STW)→ 并发标记 → 重新标记(STW)→ 并发清除。 **为什么重要**:选收集器本质是选"吞吐优先"还是"停顿优先",直接决定服务的 SLA 形态。 **误区**:以为 CMS 全程不停顿(首尾两步仍 STW);以为"并发"不花钱(抢 CPU、产生浮动垃圾,跟不上就 Concurrent Mode Failure 退化为全停顿);以为 G1 一定更快(小堆、低压力下未必优于 Parallel)。 **7. 内存分配与回收策略** 对象优先在 Eden 分配;大对象直接进老年代(`-XX:PretenureSizeThreshold`);长期存活者晋升(`-XX:MaxTenuringThreshold`,默认 15);动态年龄判定(Survivor 中同龄对象总和过半即提前晋升);空间分配担保。Minor GC 频繁而快,Full GC 稀少而慢。 **为什么重要**:这是"服务每隔几分钟抖一下"最常见的成因来源。 **误区**:以为对象都在 Eden 出生(大数组、长字符串直接进老年代,更易触发 Full GC);以为新生代越大越好(单次停顿变长,是权衡不是单向优化)。 **8. 监控工具与故障处理路径** 命令行:`jps` 找进程 → `jstat -gcutil 1000` 看各区使用率与 GC 次数/耗时 → `jmap -histo` 看对象分布、`-dump` 导堆 → MAT/jhat 分析 → `jstack` 看线程栈。可视化:JConsole、VisualVM;在线诊断另可用 BTrace 埋点。CPU 飙高的标准路径:`top -Hp` 取线程号 → 转十六进制 → `jstack` 里搜 `nid`。 **为什么重要**:工具是原理的延伸,先立假设再取证,比盲目 dump 有效得多。 **误区**:生产环境随手 `jmap -dump`(可能触发 STW)与 `jhat`(吃内存、无鉴权);只看 GC 次数不看停顿分布,错过长尾;把 CPU 高直接归因于 GC,而不看业务线程。 **9. Class 文件结构** 魔数 `0xCAFEBABE` → 次/主版本号 → 常量池(字面量 + 符号引用)→ 访问标志 → 类/父类/接口索引 → 字段表 → 方法表 → 属性表(字节码在 `Code` 属性里)。类型用描述符表示:`I`、`Ljava/lang/String;`、`[I`;方法用"名称 + 参数 + 返回值"描述符。 **为什么重要**:"一次编写,到处运行"的载体是字节码而不是 Java 语言,Kotlin/Scala 共存也靠它;能改字节码就能改行为,是 AOP、热部署、Mock、字节码增强的根。 **误区**:以为 `.class` 里留着源码信息(无注释、默认无局部变量名,反编译不可能 100% 还原)。 **10. 类加载过程与双亲委派** 加载 → 验证 → 准备(静态变量赋零值,`static int a = 1` 此刻是 0)→ 解析(符号引用转直接引用)→ 初始化(执行 ``,赋真值、跑静态块)。触发初始化的主动引用:`new`/读写静态字段/调用静态方法、反射、初始化子类前先初始化父类、主类、动态语言支持。双亲委派:先交给父加载器,父加载不了才自己加载,保证 `java.lang.Object` 全局唯一。破坏场景:SPI(线程上下文类加载器)、Tomcat 应用隔离、OSGi 网状委托。 **为什么重要**:加载时机决定静态块何时执行、单例何时创建,是"诡异初始化顺序"的根源。 **误区**:以为"类相同"只看全限定名(实为 类加载器 + 全限定名);以为双亲委派是硬性约束(只是推荐实现,可被重写 `loadClass` 打破)。 **11. 栈帧与方法调用:解析与分派** 栈帧:局部变量表(Slot 为单位,long/double 占两个)、操作数栈、动态链接、返回地址。方法调用分两类:解析(编译期即确定:静态方法、私有方法、构造器、final 方法)与分派。静态分派看"静态类型"(方法重载在编译期定版),动态分派看"实际类型"(方法重写在运行期由 `invokevirtual` 依接收者确定);虚拟机用虚方法表(vtable)把动态分派降为一次索引。 **为什么重要**:记住"重载看编译期、重写看运行期",就能解释大多数"为什么调错了方法"。 **误区**:以为重载在运行期决定;以为所有调用都走同一套分派(`invokedynamic`/MethodHandle 另有规则)。 **12. 解释执行、JIT 与编译优化** HotSpot 以解释器起步,热点方法与循环由 JIT 编译成本地代码:C1 轻量快速、C2 深度优化,热点探测分基于采样与基于计数器两种。经典优化:公共子表达式消除、数组边界检查消除、方法内联(最重要,它打开了后续优化的大门)、逃逸分析(派生栈上分配、同步消除、标量替换)。 **为什么重要**:解释器保启动速度、编译器保峰值性能,二者共存是折中;这也解释了"为什么预热之后突然变快"。 **误区**:以为 `-Xcomp` 就最快(编译开销大、启动更慢);以为逃逸分析能消灭所有分配(只对不逃逸对象有效)。 **13. Java 内存模型与 happens-before** 抽象出主内存与工作内存。三条性质:原子性(基本类型读写原子,非 volatile 的 long/double 在 32 位机上可能撕裂)、可见性(volatile、synchronized、final)、有序性(volatile 禁止重排、synchronized 保证串行)。happens-before 规则(程序次序、监视器锁、volatile、线程启动/终止/中断、对象终结、传递性)是判断"会不会看到脏值"的唯一标准。volatile 写读建立 happens-before,但不保证复合操作原子。 **为什么重要**:并发 bug 的根因多半是可见性与有序性,而不是"线程调度太诡异"。 **误区**:以为 volatile 能替代锁(`i++` 仍不安全);以为 DCL 单例里的 volatile 多余(正是它防住了指令重排)。 **14. 线程安全与锁优化** 三类手段:互斥同步(synchronized、ReentrantLock)、非阻塞同步(CAS + 原子类,注意 ABA 与自旋开销)、无同步(可重入代码、ThreadLocal、不可变对象)。锁优化:自旋与自适应自旋、锁消除(靠逃逸分析)、锁粗化、轻量级锁(CAS 抢 Mark Word)、偏向锁(同一线程重复进入只做标记)、竞争激烈后膨胀为重量级锁。 **为什么重要**:所有优化的前提都是"大多数锁其实没有竞争",理解升级路径才能解释"synchronized 为什么不再慢"。 **误区**:以为偏向锁永远更快(有撤销成本,高竞争下反而拖累);以为 CAS 无锁就没开销(自旋烧 CPU,高冲突下不如直接加锁)。 ## 三、关键概念速查表 | 概念 | 一句话解释 | 用在哪儿 | |---|---|---| | 程序计数器 | 当前线程执行字节码的行号指示器,唯一不会 OOM 的区域 | 线程切换后恢复执行位置 | | 虚拟机栈 | 线程私有,栈帧的容器,深度超限抛 StackOverflowError | 递归过深、`-Xss` 调参 | | 方法区 / 永久代 | 存类元信息、常量、静态变量;规范叫方法区,实现叫永久代 | 动态生成类过多、`intern` 撑爆 | | 直接内存 | 堆外由 NIO 直接分配的内存,不受 `-Xmx` 约束 | Netty 等高性能 IO 的堆外缓冲 | | GC Roots | 可达性分析的起点集合(栈引用、静态属性、常量、JNI) | 判断对象是否可回收 | | 安全点 / 安全区域 | 线程能停下让 GC 扫描的位置 | 解释 STW 为何能"停下来" | | 卡表 / 记忆集 | 记录老年代指向新生代的跨代引用 | 避免 Minor GC 扫全堆 | | TLAB | 线程私有的分配缓冲区 | 解决并发分配对象的竞争 | | 分配担保 | 新生代放不下时由老年代兜底 | 复制算法只需 10% 预留的原因 | | 双亲委派 | 先父后子的加载顺序 | 保证核心类唯一、防止被替换 | | `` | 类初始化时执行的静态块与静态赋值 | 静态变量何时有真值 | | 虚方法表 vtable | 每个类一张,按槽位存实际方法入口 | 动态分派的一次索引实现 | | 逃逸分析 | 判断对象是否逃出方法/线程 | 栈上分配、锁消除、标量替换 | | happens-before | 定义两次操作的可见性先后 | 判断并发代码是否安全 | | CAS | 比较并交换的 CPU 原子指令 | 原子类、轻量级锁、无锁结构 | | Mark Word | 对象头中记录 hash、GC 年龄、锁状态的字 | 锁升级的状态载体 | ## 四、可复用的方法/框架 **A. OOM 五步定位法** 1. 读异常首行,确定区域(heap / permgen / native thread / direct memory); 2. 确认是否有 `-XX:+HeapDumpOnOutOfMemoryError` 留下的现场,没有则复现并主动 dump; 3. MAT 打开支配树,按 retained size 排序找最大对象; 4. 沿引用链回溯到 GC Root,判断是"业务该释放没释放"还是"缓存无上限"; 5. 先修代码,再评估容量,最后才谈参数。 **B. GC 调优四问** 停顿有多长(看分布不看均值)→ 频率有多高 → 是 Minor 还是 Full → 目标是什么(低延迟还是高吞吐)。四问答不上来就不要动参数;参数永远是最后一步,第一步是减少对象产生速率。 **C. 类加载问题的三问** 这个类由哪个加载器加载(打印 `getClassLoader()`)→ 是否被双亲委派覆盖 → 是否存在同名不同加载器的"类冲突"(`ClassCastException` 的常见真凶)。 **D. 锁问题判断清单** 临界区是否真的需要锁 → 锁粒度能否缩小 → 是否存在无竞争的锁(可否消除)→ 同一线程是否反复进出(可否粗化/偏向)→ 冲突是否高到自旋不划算(是否直接上重量级)。 **E. 读这类底层书的方法** 先建"数据面 + 执行面"两张图,再把每个故障现象挂到图上某个区域;每读完一章,用一个自己项目里的真实故障去套,套不上说明没读懂。 ## 五、我的阅读轨迹 | 项目 | 数据 | |---|---| | 进度 | 100% | | 累计阅读 | 8 小时 31 分钟 | | 开始阅读 | 2019-03-10 | | 最近阅读 | 2021-04-21 | | 读完时间 | 2019-03-30 | **节奏评估**:20 天读完 8.5 小时,属于"通读一遍"而非"精读消化",第二、三部分(内存与执行子系统)大概率只留了印象,案例章(第 5、9 章)容易被跳过。 **复习建议**: - 第一优先级:第 3 章 + 第 4 章。这两章是日常排障的高频区,且本书以 JDK 1.6 为背景,须对照元空间、G1 重学一遍。 - 第二优先级:第 7、8 章。类加载与分派是面试与框架源码(Spring、Tomcat)的公共基础。 - 第三优先级:第 13 章。锁升级路径要与《Java 并发编程的艺术》对照读,单看本书易停留在名词层。 - 可速览:第一部分与附录,查表用即可;第 5、9 章建议在有过一次真实故障后再回读,收益最大。 ## 六、行动清单 1. 用 `jps` + `jstat -gcutil` 观察一个线上/本地进程 10 分钟,记录 GC 次数与停顿分布。 2. 故意写一个 `OutOfMemoryError: Java heap space`,用 `HeapDumpOnOutOfMemoryError` + MAT 走完一次完整定位。 3. 用 `javap -v` 反编译一个自己写的类,逐段对上常量池、字段表、方法表与 `Code` 属性。 4. 写一个重载 + 重写的例子,验证"编译期定重载、运行期定重写"。 5. 写 DCL 单例,去掉 `volatile` 并压测,观察是否出现半初始化对象。 6. 用 `-XX:+PrintGCDetails` 对比 Serial 与 Parallel 在小堆下的停顿差异,记录数据而非感觉。 7. 把本文"关键概念速查表"做成 16 张卡片,两周内过三轮。 8. 对照 JDK 8+ 文档,列出本书中已过时的三处描述(永久代、CMS 默认地位、G1 试验状态),标注在书页上。 ## 七、延伸阅读 1. **《Java 虚拟机规范(Java SE 8 版)》**——本书的"上位文档"。先用本书建立直觉,再用规范分清"哪些是规范强制、哪些只是 HotSpot 的实现选择",顺序不能反,否则读不下去。 2. **《Java 并发编程实战》**——本书第五部分的工程化展开。读完第 13 章立刻接这本,把 happens-before 与锁优化落到 `java.util.concurrent` 的具体组件上。 3. **《Java 性能权威指南》**——把本书第 4、5 章的工具与案例升级为系统化性能方法论,适合在有真实调优任务时读,放在最后。