# 《剑指JVM:虚拟机实践与性能调优》知识点全解 > AI生成|2026年10月28日|codebuddy|原书:尚硅谷教育《剑指JVM:虚拟机实践与性能调优》,清华大学出版社,2023-05-01 > **我的阅读**:进度 100%|累计 9小时|开始 2023-10-15|最近 2023-11-21|读完 2023-10-29 **一句话总结**:把 Java 程序从“字节码如何被执行”到“内存如何布局、对象如何回收、类如何加载、线上如何观测与调优”串成一条完整链路,帮助你用 JVM 的结构化模型解释并解决真实的性能问题。 **适合谁 / 前置知识**:适合已有 Java 基础、准备面试或需要排查 OOM、GC 停顿、CPU 飙高的后端开发者;也适合想从“会写代码”进阶到“理解运行时”的工程师。需要会写 Java、了解线程与基本数据结构,知道命令行和 Linux 常用操作。不适合零编程基础读者,也不适合想深入研究编译器实现或 JVM 源码的读者——本书重点是运行时机制与工程实践,不是编译器理论专著。 ## 一、全书地图 | 篇章 | 覆盖章节 | 回答的核心问题 | 与其他部分的关系 | |---|---|---|---| | 引言篇 | 无编号章节 | Java 与 JVM 是什么关系,JVM 由哪些部分组成 | 建立总图,为后续所有章节提供坐标 | | 第1篇 运行时数据区 | 第3—12章 | 程序运行时数据放在哪里,字节码如何被执行 | 全书地基;理解堆、栈、方法区后才谈得上 GC | | 第2篇 垃圾收集 | 第14—16章 | 哪些内存可以回收,何时回收,用什么收集器 | 直接建立在对堆、方法区、对象布局的理解上 | | 第3篇 字节码与类的加载 | 第18—20章 | `.class` 文件如何变成可用的类,类加载器如何协作 | 解释方法区、常量池、执行引擎的前置机制 | | 第4篇 性能监控与调优 | 第22—26章 | 如何观测 JVM、定位问题、验证调优效果 | 前三篇的工程落地,形成“原理→诊断→优化”闭环 | 逻辑主线可以概括为:**代码如何变成字节码 → 字节码在哪些内存区域运行 → 不再使用的对象如何被回收 → 类如何被加载和隔离 → 出问题时如何观测和调优**。第1篇是静态结构,第2篇是动态回收,第3篇是“类”的生命周期,第4篇是把前三者用于生产诊断。 ## 二、核心知识点 ### 1. JVM、字节码与跨平台 **是什么**:JVM 是执行 `.class` 字节码的抽象计算机,核心包括类加载子系统、运行时数据区、执行引擎和本地接口。 **为什么重要**:Java 的跨平台不是源码直接跨平台,而是“源码→统一字节码→各平台 JVM”的分层;理解这层,才能理解内存模型和调优边界。 **怎么用/例子**:`javac Hello.java` 后,用 `javap -c -v Hello.class` 查看常量池、方法指令和行号表;同一个 class 文件可在不同平台的 JVM 上运行。 **常见误区**:把 JVM 等同于 Java 语言;以为字节码就是机器码。字节码由解释器或 JIT 转成机器指令,JVM 也能运行 Kotlin、Scala 等语言。 ### 2. 程序计数器与虚拟机栈 **是什么**:程序计数器线程私有,记录当前线程下一条字节码地址;虚拟机栈也线程私有,以栈帧为单位保存局部变量表、操作数栈、动态链接和返回地址。 **为什么重要**:二者决定方法调用、递归深度和线程上下文切换;栈溢出、线程创建失败常从这里定位。 **怎么用/例子**:递归过深时调整 `-Xss` 并观察 `StackOverflowError`;用 `jstack` 看线程栈和方法调用链,区分死锁、阻塞与递归。 **常见误区**:以为程序计数器也会 OOM(规范上不会);以为局部变量里的对象一定在栈上。栈帧保存的是引用,对象通常在堆中,逃逸分析只是优化手段。 ### 3. 堆与对象分配 **是什么**:堆线程共享,主要存对象实例和数组;HotSpot 按分代管理 Eden、两个 Survivor 和 Old,TLAB 让线程在 Eden 内快速分配。 **为什么重要**:堆是 GC 主战场,也是 `OutOfMemoryError: Java heap space` 的主要来源;分配速率和晋升速率决定 GC 频率。 **怎么用/例子**:`-Xms` 与 `-Xmx` 设成一致以减少抖动;`jmap -histo` 看对象数量,`jmap -dump` 配合 MAT 看支配树;大对象可能绕过 Eden 直接进入老年代,具体取决于收集器和参数。 **常见误区**:以为所有对象都先分配在 Eden;以为新生代与老年代比例固定不变;把 `-Xmn` 当成所有收集器都推荐的参数。 ### 4. 方法区、元空间与常量池 **是什么**:方法区是规范中的共享区域,存类元数据、字段和方法描述、运行时常量池等;JDK 8 起 HotSpot 用本地内存中的元空间实现,替代永久代。 **为什么重要**:类加载、反射、动态代理和脚本引擎都会产生元数据;元空间泄漏表现为 `Metaspace` OOM,且堆 dump 不一定能直接看到。 **怎么用/例子**:用 `-XX:MetaspaceSize` 设初始高水位、`-XX:MaxMetaspaceSize` 设上限;用 `jstat -gcmetacapacity` 观察;动态生成类过多时,排查类加载器是否被回收。 **常见误区**:认为 JDK 8 后仍有永久代、`-XX:MaxPermSize` 仍有效;把字符串常量池说成在元空间,实际上 JDK 7 起已在堆中。 ### 5. 对象实例化、内存布局与访问定位 **是什么**:对象创建大致经历类加载检查、分配内存、零值初始化、设置对象头、执行构造方法;布局为对象头(Mark Word、类型指针)、实例数据和对齐填充。 **为什么重要**:对象大小影响缓存局部性、堆占用和 GC 复制成本;对象头承载哈希、GC 年龄和锁状态;访问方式影响解引用开销。 **怎么用/例子**:64 位 HotSpot 开启压缩指针时引用通常为 4 字节,对象按 8 字节对齐;`new` 得到的栈上引用通常直接指向堆对象,HotSpot 常用直接指针访问。 **常见误区**:以为对象头只有 Mark Word;以为字段严格按源码顺序排列;以为所有 JVM 都通过句柄池访问对象。 ### 6. 直接内存 **是什么**:直接内存是堆外由本地内存分配的缓冲区,典型是 `ByteBuffer.allocateDirect()`,也可由 JNI 或 Unsafe 分配;不受堆大小限制,但受 `-XX:MaxDirectMemorySize` 等约束。 **为什么重要**:NIO、Netty 等可减少堆内外复制,提升 I/O 性能;但堆外泄漏难排查,可能报 `Direct buffer memory` OOM。 **怎么用/例子**:按业务设置直接内存上限;用 NMT(`-XX:NativeMemoryTracking=summary` 加 `jcmd VM.native_memory`)观察;直接缓冲由 Cleaner 在合适时机回收,不要假设立即释放。 **常见误区**:以为直接内存属于堆;以为 direct buffer 永远比堆内快;以为调用 `System.gc()` 就能可靠清空。 ### 7. 执行引擎:解释器、JIT 与去优化 **是什么**:执行引擎把字节码变为机器执行,包含解释器和即时编译器;HotSpot 的 C1/C2 与分层编译协调启动速度和峰值性能。 **为什么重要**:同一程序冷启动和稳定运行表现不同;JIT 会内联、做逃逸分析、锁消除,也会因假设失效而去优化,性能曲线并非线性。 **怎么用/例子**:用 `-XX:+PrintCompilation` 看编译和去优化事件;短生命周期任务关注启动开销,长服务关注稳态吞吐和代码缓存。 **常见误区**:以为 JIT 会编译所有方法;以为解释器没有价值;以为加 `-Xcomp` 一定更快,实际常因编译开销和代码质量变慢。 ### 8. 字符串常量池与 `intern` **是什么**:字符串字面量在类常量池中,运行时可解析到堆中的字符串常量池(StringTable);`intern()` 返回池中的规范引用。 **为什么重要**:字符串是高频大对象,重复字面量可以共享;但大量动态字符串 intern 会造成池膨胀和 GC 压力。 **怎么用/例子**:`new String("a") == "a"` 为 false,`new String("a").intern() == "a"` 为 true;大量重复的枚举值或配置键可考虑 intern,但应先测量。 **常见误区**:以为字符串常量池在元空间;以为 `intern()` 一定复制新对象;用 `==` 比较所有字符串。 ### 9. 垃圾判定与收集算法 **是什么**:主流 JVM 用可达性分析,从 GC Roots(栈引用、静态引用、JNI 引用等)出发标记存活对象;基础算法有标记-清除、复制和标记-整理。 **为什么重要**:算法决定碎片程度、复制成本和停顿形态;分代收集利用“大多数对象朝生夕死”的经验规律。 **怎么用/例子**:新生代常用复制算法,老年代常用标记-整理或标记-清除;排查内存泄漏时先看 GC 后老年代是否仍持续上涨,而不是只看单次占用。 **常见误区**:以为 JVM 主要靠引用计数;以为对象一不可达就立刻释放;以为标记-清除没有碎片问题。 ### 10. GC 指标与分代事件 **是什么**:核心指标有吞吐量、最大/平均停顿和内存占用;事件有 Minor/Young GC、Major/Old GC 和 Full GC,STW 表示应用线程暂停。 **为什么重要**:调优是在三者之间权衡,不是单纯“GC 越快越好”;指标选错会导致参数越调越糟。 **怎么用/例子**:用 `-Xlog:gc*`(JDK 9+)或旧版 GC 日志看停顿、回收量和晋升量;先确定 SLO(如 P99 延迟、吞吐量),再选择优化指标。 **常见误区**:把低延迟当作唯一目标;以为 Full GC 只回收老年代;以为 GC 线程越多越好,线程过多会抢占 CPU。 ### 11. 垃圾收集器选型 **是什么**:常见收集器包括 Serial、ParNew、Parallel Scavenge/Old、CMS、G1、ZGC 等,按分代方式、并发能力、堆规模和停顿目标区分。 **为什么重要**:JDK 8 服务器默认常为 Parallel Scavenge 加 Parallel Old,JDK 9+ 默认 G1;选错会让延迟或吞吐不达标。 **怎么用/例子**:批处理重吞吐可选 Parallel;大堆且要求可控停顿选 G1(`-XX:+UseG1GC`、`-XX:MaxGCPauseMillis`);超低延迟大堆评估 ZGC;CMS 在新版 JDK 已移除,老项目才需维护。 **常见误区**:以为 G1 完全没有 STW(Young/Mixed GC 仍有);以为 ZGC 绝对零停顿;不看 JDK 版本照搬参数。 ### 12. 类加载过程与类加载器 **是什么**:类加载通常分加载、验证、准备、解析和初始化;准备阶段给静态变量赋零值,初始化阶段执行 ``;类加载器负责定位和定义类。 **为什么重要**:懒加载、双亲委派、类隔离、热部署和 SPI 都依赖这套机制;类冲突和元空间泄漏常源于加载器。 **怎么用/例子**:用 `-verbose:class` 看类的加载来源;自定义 ClassLoader 实现隔离;SPI 场景用线程上下文类加载器打破“父加载器看不到子类实现”的限制。 **常见误区**:把加载等同于初始化;以为准备阶段就执行静态赋值;以为双亲委派绝对不能打破。 ### 13. 字节码指令与 `javap` **是什么**:Class 文件包含常量池、方法表和属性等;字节码是基于操作数栈的指令集,常见指令有 `iload`、`invokevirtual`、`invokestatic`、`monitorenter/exit`。 **为什么重要**:语法糖、泛型擦除、异常表和动态分派最终都落在字节码上;定位“源码看不出”的问题需要读指令。 **怎么用/例子**:`javap -c -p` 看方法指令,`javap -v` 看常量池和行号表;结合 ASM、ByteBuddy 理解字节码增强和 AOP。 **常见误区**:以为一行源码对应一条指令;以为所有方法调用都是虚调用;把字节码当成机器码。 ### 14. 监控工具链与调优闭环 **是什么**:常用工具包括 `jps`、`jstat`、`jmap`、`jstack`、`jcmd`、`jinfo`,以及 JFR、VisualVM、MAT、Arthas;调优闭环是“基线→观测→假设→最小变更→回归”。 **为什么重要**:没有证据的参数调整只是赌博;OOM 类型不同,排查路径完全不同。 **怎么用/例子**:`jstat -gcutil 1000` 看 GC 趋势;`jstack` 看线程阻塞;`jmap -dump:live,format=b,file=heap.hprof ` 做堆分析;生产环境加 `-XX:+HeapDumpOnOutOfMemoryError`;堆 OOM、Metaspace、Direct buffer、无法创建线程分别查对象、类加载器、堆外缓冲和线程数。 **常见误区**:一出问题就调 `-Xmx`;只看一次快照不看趋势;把工具输出当结论,不做对照实验。 ## 三、关键概念速查表 | 概念 | 一句话解释 | 用在哪儿 | |---|---|---| | 字节码 | JVM 执行的平台无关指令集,存放在 `.class` 文件 | 跨平台、反编译、字节码增强 | | 栈帧 | 一个方法调用对应的运行时结构,含局部变量表和操作数栈 | 方法调用、递归、栈溢出分析 | | TLAB | 线程在 Eden 中独占的小块分配缓冲区 | 降低多线程分配竞争 | | 元空间 | JDK 8+ HotSpot 对方法区的实现,使用本地内存 | 类元数据、动态代理、Metaspace OOM | | 运行时常量池 | Class 常量池在运行时的表示,存放字面量和符号引用 | 字符串、类解析、反射 | | 对象头 | 对象开头的管理信息,含 Mark Word 和类型指针 | 哈希、GC 年龄、锁状态、对象大小 | | 压缩指针 | 64 位 JVM 用 32 位表示引用的优化 | 降低对象引用开销,通常堆小于 32GB 时启用 | | 直接内存 | 堆外由本地内存分配的缓冲区 | NIO、Netty、堆外 OOM | | JIT | 运行时把热点字节码编译为机器码 | 稳态性能、去优化、代码缓存 | | GC Roots | 可达性分析的起点,如栈引用、静态引用、JNI 引用 | 判断对象是否存活 | | STW | Stop The World,GC 时暂停应用线程 | 延迟分析、收集器选型 | | 分代收集 | 按对象年龄把堆分为新生代和老年代分别回收 | Minor GC、晋升、老年代泄漏 | | 双亲委派 | 类加载器先委托父加载器加载,失败再自己加载 | 类隔离、SPI、容器与插件 | | OOM | 内存或资源不足抛出的错误,可能是堆、元空间、直接内存或线程 | 线上故障定位与容量规划 | ## 四、可复用的方法/框架 ### 1. 性能问题六步闭环 1. **定义目标**:先写清楚是降 P99 延迟、提吞吐,还是降内存占用;没有目标就没有“调优成功”。 2. **固化基线**:记录 JDK 版本、JVM 参数、机器规格、流量模型、GC 日志和关键业务指标。 3. **分层定位**:按“操作系统/容器→JVM→应用代码→下游依赖”逐层排除,避免一上来就改 JVM 参数。 4. **提出假设**:例如“老年代增长来自缓存无上限”“停顿来自 Young GC 频繁”,假设必须能被数据证伪。 5. **最小变更**:一次只改一个变量,保留回滚方案;参数调整要能解释其作用机制。 6. **回归验证**:在相同负载下对比基线,确认延迟、吞吐、错误率和资源使用没有此消彼长。 ### 2. 内存问题判断清单 - **Java heap space**:先看堆 dump,按支配树找 retained size 最大的对象,再回到业务代码找持有链。 - **Metaspace**:查动态类生成、热部署、类加载器泄漏;堆 dump 不是唯一证据。 - **Direct buffer memory**:查 NIO/Netty 的直接缓冲分配与释放,核对 `MaxDirectMemorySize`。 - **unable to create new native thread**:查线程数、线程池上限、系统 `ulimit` 和容器限制,不要只加堆。 - **GC overhead limit exceeded**:通常不是“内存太小”这么简单,而是回收效率过低,需看存活对象和分配速率。 ### 3. 收集器选择顺序 先看 JDK 版本和可用收集器,再看堆规模与延迟目标:小堆或客户端场景可接受 Serial;批处理优先 Parallel 的吞吐;大堆且要求可控停顿优先 G1;极低延迟且堆很大时评估 ZGC。任何选择都要用实际负载验证,不照搬“最佳实践”。 ### 4. 参数调整四原则 - 先测量后调整,参数是假设的实验条件,不是答案。 - `-Xms` 与 `-Xmx` 尽量一致,减少堆伸缩带来的抖动。 - 一次只改一个参数,记录变更前后至少一组可对比指标。 - 生产环境保留 GC 日志和 OOM dump 能力,再谈优化。 ## 五、我的阅读轨迹 | 指标 | 数据 | |---|---| | 进度 | 100% | | 累计阅读 | 9小时 | | 开始阅读 | 2023-10-15 | | 最近阅读 | 2023-11-21 | | 读完时间 | 2023-10-29 | 你已完成首轮阅读,没有“未读完章节”需要补课。二轮复习建议把时间集中在高杠杆章节:第7、8章重画堆与方法区图;第16章做收集器对比表;第18—20章配合 `javap` 和自定义类加载器做实验;第22、24、25、26章复盘工具命令、GC 日志和 OOM 案例。每隔一段时间用“能否不看书画出内存结构并解释一次 Full GC”自测,比重复通读更有效。 ## 六、行动清单 - 用 `javap -c -p` 反编译一个包含继承、泛型和异常处理的方法,标注常量池与调用指令。 - 写三个最小 demo:栈溢出、堆 OOM、Metaspace OOM,分别采集日志并记录报错特征。 - 在本机服务上采集 5 分钟 `jstat -gcutil` 和 GC 日志,画出分配与晋升基线。 - 用 MAT 打开一次 heap dump,按支配树找出 retained size 最大的对象及持有链。 - 整理一张收集器选型表,写清 JDK 版本、堆规模、停顿目标和适用场景。 - 给一个服务配置 `-XX:+HeapDumpOnOutOfMemoryError` 与统一日志路径,并验证能成功落盘。 - 每月用 30 分钟回看第7、8、16、22、24章,更新自己的 JVM 参数笔记。 ## 七、延伸阅读 1. **《深入理解Java虚拟机:JVM高级特性与最佳实践》周志明**:与本书主题最接近,理论体系更完整,适合作为第二轮系统精读;建议先读本书建立框架,再读它补齐细节与版本差异。 2. **《Java性能权威指南》Scott Oaks**:偏性能方法论、工具使用和调优实践,适合在本书第4篇之后阅读,把“会看工具”推进到“会做实验”。 3. **《Java虚拟机规范》Tim Lindholm 等**:规范类资料,不适合通读;遇到字节码、类加载、运行时行为边界时按需查阅,放在最后作为工具书。 建议顺序:**本书打底 → 《深入理解Java虚拟机》系统化 → 《Java性能权威指南》工程化 → 《Java虚拟机规范》按需查证**。