
简介《深入理解Java虚拟机JVM高级特性与最佳实践第3版》电子版面向具备一定Java基础、希望系统掌握JVM原理与调优的开发者与面试备考者围绕内存管理、执行子系统与性能诊断三大主线展开。内容从Java技术体系与虚拟机家族讲起深入运行时数据区域、OutOfMemoryError实战、垃圾收集算法与Serial至ZGC各代收集器并延伸至类文件结构、类加载机制、字节码执行引擎配合jps、jstat、JConsole、VisualVM等监控工具与调优案例形成从原理到排错的完整链路。资源包共1个PDF文件约28.65MB为机械工业出版社2019年版电子书目录完整、章节层级清晰便于按模块检索与对照学习。目前已有750人学习下载适合作为案头工具书反复查阅也可用于面试前梳理知识框架、排查线上内存与GC问题。1. 从一次线上 Full GC 长停顿说起这本 JVM 手册到底能解决什么去年帮一个团队排查接口超时监控上 CPU 不高、内存也没打满但每隔十几分钟就有一批请求集体卡住两三秒。日志里翻到Full GC字样年轻同事的第一反应是“加内存”结果堆从 4G 加到 8G停顿反而更长了。问题不在内存大小而在于没人说得清对象是怎么被判定为垃圾、收集器在哪个阶段停了整个应用、-XX参数之间又怎么互相牵制。这类“黑匣子”式的故障靠搜索引擎拼凑答案往往越查越乱需要一本能把运行时数据区、垃圾收集算法、类加载、即时编译、内存模型串成一条线的系统性资料。《深入理解Java虚拟机JVM高级特性与最佳实践第3版》就是干这个的。它不讲 Java 语法也不教框架用法而是把 HotSpot 这台“发动机”拆开给你看内存怎么划分、对象怎么创建和访问、垃圾怎么回收、字节码怎么执行、JIT 怎么优化、并发底层靠什么保证。第 3 版把内容跟进到了 JDK 12/13 的时间点补上了 G1 的完整解析、ZGC 与 Shenandoah 两款低延迟收集器、Java 模块化对类加载的影响、Graal 编译器与 JVMCI 接口以及协程相关的讨论。适合已经能写业务代码、但一遇到 OOM、CPU 飙高、类冲突就发怵的中高级开发也适合要把调优经验沉淀成团队规范的系统调优角色。下面按“先立原理、再动手复现、最后避坑”的顺序把它拆成能照着做的实战笔记。2. 内存区域与 OOM 复现把运行时数据区跑成可观测的现场2.1 七个运行时数据区先分清谁共享谁私有书里第 2 章把虚拟机内存切成程序计数器、虚拟机栈、本地方法栈、Java 堆、方法区、运行时常量池、直接内存七块。真正决定排查方向的是“线程私有”和“线程共享”这条分界线程序计数器、虚拟机栈、本地方法栈随线程生灭栈溢出基本是递归太深或栈帧过大堆和方法区JDK 8 之后叫元空间是全局共享溢出往往意味着对象没被释放或类加载器泄漏。直接内存虽然不在堆里但-XX:MaxDirectMemorySize不设时默认约等于堆上限NIO 用多了照样能把物理内存吃穿。第 3 版特别修正了第 2 版对虚拟机栈描述的含糊之处并明确写了 JDK 8 永久代退场、元空间登场的背景。这一点很关键很多老博客还在教人调-XX:PermSize在 JDK 8 以上这些参数直接失效照着抄只会得到“Unrecognized VM option”的报错。我一般会先让读者用一条命令确认自己到底在哪个版本上折腾# 确认 JDK 版本与虚拟机实现参数是否生效以此为准 java -version # 输出示例openjdk version 11.0.x ... 64-Bit Server VMjava -version第二行的Server VM说明用的是 HotSpot 服务端模式后面所有-XX参数才有讨论意义。如果显示的是别的虚拟机实现书里大部分以 HotSpot 为对象的参数和工具都要打折扣看。2.2 用最小代码把四类 OOM 逐个打出来光看区域划分记不住得亲手触发。书里 2.4 节给了堆溢出、栈溢出、方法区与常量池溢出、直接内存溢出四组实战代码。下面这组是我按书里思路整理的最小复现每个都能独立跑方便对照异常信息里的区域名import java.util.ArrayList; import java.util.List; public class OomDemo { // 堆溢出不断往集合里塞对象且保持强引用不被回收 static void heapOom() { Listbyte[] list new ArrayList(); while (true) { list.add(new byte[1024 * 1024]); // 每次 1MB } } // 栈溢出无限递归栈帧不断压入直到超过 -Xss 限制 static void stackOverflow() { stackOverflow(); } public static void main(String[] args) { String mode args.length 0 ? args[0] : heap; if (heap.equals(mode)) { heapOom(); } else if (stack.equals(mode)) { stackOverflow(); } } }堆溢出这组要配-Xms20m -Xmx20m -XX:HeapDumpOnOutOfMemoryError跑堆固定小一点几秒就能看到java.lang.OutOfMemoryError: Java heap space同时当前目录会落一个.hprof文件后面用可视化工具打开就能看到是谁占着内存不放。栈溢出那组把-Xss调小比如-Xss256k更容易触发异常是StackOverflowError注意它和 OOM 不是一回事前者是栈深度超限后者是内存申请失败。方法区溢出在 JDK 8 以后要靠不停生成动态类来打常见做法是用 CGLIB 或反射批量定义类配合-XX:MaxMetaspaceSize10m观察Metaspace溢出直接内存溢出则用ByteBuffer.allocateDirect循环申请配-XX:MaxDirectMemorySize10m异常信息里会出现Direct buffer memory。这四组跑完你对“哪个区域出问题会报什么错”就有了肌肉记忆比背十遍区域图管用。提示HeapDumpOnOutOfMemoryError在生产上建议常开但 dump 路径要指到大盘别把系统盘写满这是血泪经验。3. 垃圾收集器选型与日志解读从 Serial 到 ZGC 怎么挑3.1 先判断对象死活再谈收集器书里第 3 章的逻辑很顺先回答“对象已死吗”再讲怎么收。引用计数因为循环引用问题在主流虚拟机里不用HotSpot 走的是可达性分析从 GC Roots 出发标记存活对象。GC Roots 包括虚拟机栈中引用的对象、方法区静态属性引用的对象、常量引用的对象、本地方法栈 JNI 引用的对象等。理解这一点你才能明白为什么一个静态Map缓存能把整批对象钉死在内存里——它本身就是 GC Root 可达的。第 3 版新增了记忆集与卡表、写屏障、增量更新与原始快照这几节专门解释跨代引用和并发标记怎么处理。这部分以前是源码级的门槛现在被拉到了原理层新生代收集时不可能全堆扫描老年代于是用卡表记录“老年代哪块引用了新生代”写屏障负责在引用变化时维护这张表。CMS 用增量更新解决并发标记漏标G1 用原始快照两者取舍不同。知道这层你再看收集器日志里的并发标记阶段就不会一头雾水。3.2 收集器怎么选一张对照表定方向书里覆盖了 Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1以及低延迟的 Shenandoah、ZGC还提了 Epsilon 这个“只分配不回收”的测试用收集器。选型不是越新越好得看堆大小、停顿目标和 JDK 版本。我按实战经验整理成下面这张表参数名以 HotSpot 为准收集器组合启用参数适用场景主要代价Serial Serial Old-XX:UseSerialGC单核、小堆、客户端全程 STW堆大就停得久Parallel Scavenge Parallel Old-XX:UseParallelGC吞吐优先、批处理停顿不可控ParNew CMS-XX:UseConcMarkSweepGC老年代低停顿JDK 14 前碎片、并发模式失败G1-XX:UseG1GC大堆、可预测停顿参数调优有门槛ZGC-XX:UseZGC超大堆、极低停顿需较新 JDK吞吐略降CMS 在 JDK 9 被标记废弃、JDK 14 移除如果线上还在用升级路径要提前规划G1 从 JDK 9 起成为默认收集器-XX:MaxGCPauseMillis设一个合理目标比如 200ms它会自己调整分区回收节奏但别把它当硬保证。ZGC 在 JDK 11 是实验特性要加-XX:UnlockExperimentalVMOptions才能开JDK 15 之后转正这个版本差异不搞清楚参数写了也不生效。3.3 打开 GC 日志用 jstat 做在线观测调优不能靠猜。JDK 9 之后统一用-Xlog系列参数老版本的-XX:PrintGCDetails已经变了写法这是最容易翻车的地方# JDK 9 统一日志格式输出到文件并按大小轮转 java -Xms512m -Xmx512m \ -Xlog:gc*,gcheapdebug,gcagetrace:filegc.log:time,uptime,level,tags:filecount5,filesize20m \ -jar app.jargc*表示打开所有 gc 相关标签gcheapdebug补充堆前后变化filecount和filesize控制轮转避免日志撑爆磁盘。日志里重点看三样每次停顿的Pause时间、回收前后堆占用、以及是否出现Allocation Failure频繁触发 Young GC。如果 Young GC 很频繁但每次回收后老年代占用持续上涨基本可以判定有对象在往老年代沉淀得回去查缓存或大对象分配。在线观测用jstat它不需要重启进程# 每 1 秒采样一次共 10 次观察 Eden/Old 变化与 GC 次数 jstat -gcutil pid 1000 10输出里E是 Eden 使用率、O是老年代使用率、YGC/FGC是次数、FGCT是 Full GC 累计耗时。如果O长期在 90% 以上且FGC持续增长说明老年代回收不动了这时候再考虑 dump 堆分析而不是盲目调大-Xmx。4. 类加载、字节码与 JIT执行子系统怎么排错4.1 双亲委派被破坏时先看类加载器层级第 7 章讲类加载的五个阶段加载、验证、准备、解析、初始化。准备阶段给静态变量赋零值初始化阶段才执行clinit这个顺序决定了“静态变量在什么时候有值”。双亲委派模型保证java.lang.*这类核心类不会被自定义类覆盖但 Tomcat 的 Web 应用隔离、OSGi 的模块化、JDBC 的 SPI 机制都会主动破坏它。线上遇到ClassNotFoundException或NoSuchMethodError八成是同一个类被不同加载器加载了两份或者依赖版本冲突。排查手段是让类加载过程可见# 打印类加载与卸载详情定位重复加载和加载器来源 java -verbose:class -jar app.jar 21 | grep com.example-verbose:class会输出每个类由哪个加载器加载配合grep过滤业务包名能快速看出同一个类是否出现两次。JDK 9 引入模块化后类加载器结构变成引导加载器、平台加载器、系统加载器三层书里 7.5 节专门讲了模块兼容性和模块化下的加载器变化升级 JDK 时这块要重点回归。4.2 用 javap 读字节码理解语法糖的真相第 6 章把 Class 文件结构拆到魔数、常量池、访问标志、字段表、方法表、属性表第 10 章又讲了泛型擦除、自动装箱、条件编译这些语法糖。光看文字容易飘用javap反编译一段代码就落地了# 反编译字节码-c 显示指令-p 显示私有成员 javap -c -p -v target/classes/com/example/Demo.class重点看泛型方法反编译后类型参数是不是变成了Object加强制转换类型擦除看for循环遍历集合是不是被编译成了迭代器调用看Integer自动装箱是不是调了Integer.valueOf。这些细节决定了你写出的代码在字节码层面到底长什么样也解释了为什么“泛型不能new T()”“Integer缓存范围是 -128 到 127”这类面试题背后的机制。4.3 JIT 编译结果怎么看别把解释执行当性能瓶颈第 11 章讲即时编译器、热点探测、编译触发条件以及方法内联、逃逸分析、公共子表达式消除这些优化技术。实战里最有用的是观察某段代码到底有没有被 JIT 编译。常见做法是加-XX:PrintCompilation输出里带%的是 OSR 编译循环内即时编译带made not entrant的是去优化。如果一段热点方法迟迟不出现编译记录可能是方法太大超过-XX:MaxInlineSize或-XX:FreqInlineSize阈值也可能是解释器阶段就卡住了。第 3 版新增的 Graal 编译器和 JVMCI 接口属于进阶内容日常调优用不到但理解“编译器本身也可以被替换”这件事对判断某些“换 JDK 就变快”的玄学现象有帮助。我的习惯是性能问题先排除锁竞争和 IO再怀疑 GC最后才看 JIT顺序反了容易在编译器上浪费大量时间。5. 调优避坑五个真实翻车现场与排查路径5.1 堆越大停顿越短不一定现象把-Xmx从 4G 调到 16GFull GC 频率降了但单次停顿从 1 秒涨到 5 秒。原因Serial Old 或 Parallel Old 做全堆整理时堆越大扫描和移动的对象越多停顿线性增长。解决大堆场景换 G1 或 ZGC用分区回收把停顿拆碎同时设-XX:MaxGCPauseMillis给个目标值别指望它精确兑现。5.2 元空间不设上限容器里被 OOMKilled现象应用在容器里跑着跑着被系统杀掉日志里没有 Java OOM。原因-XX:MaxMetaspaceSize没设动态生成类多时元空间持续增长容器内存超限被内核干掉。解决显式设-XX:MaxMetaspaceSize并配-XX:MetaspaceSize作为初始阈值同时给容器留出堆外内存余量。5.3 安全点导致“无 GC 也停顿”现象GC 日志里没有长停顿但监控显示应用周期性卡顿。原因JVM 进入安全点需要所有线程到达安全点线程在计数循环里长时间不轮询安全点状态就会拖住整个停顿。书里 5.2.8 节专门讲过这个案例。解决排查大循环必要时用-XX:PrintSafepointStatistics观察安全点耗时把长循环拆小或插入可安全点轮询的调用。5.4 堆外内存泄漏堆 dump 看不出来现象物理内存持续上涨堆 dump 却很正常。原因NIO 的DirectByteBuffer走直接内存不受堆大小约束-XX:MaxDirectMemorySize没设时上限约等于堆上限。解决显式设置直接内存上限用jcmd pid VM.native_memory需开-XX:NativeMemoryTracking或系统层工具观察别只盯着堆。5.5 照抄老参数新 JDK 直接起不来现象从 JDK 8 升到 JDK 11启动报Unrecognized VM option UseConcMarkSweepGC或PrintGCDetails。原因CMS 被移除、GC 日志参数统一到-Xlog、永久代参数失效。解决升级前用java -XX:PrintFlagsFinal -version对照参数是否还存在把-XX:PrintGCDetails换成-Xlog:gc*把PermSize换成MetaspaceSize逐项迁移而不是整段复制。6. 把书里的案例变成自己的验证脚本一个可复用的调优闭环书里第 5 章的调优案例和 Eclipse 实战价值不在结论而在“观测—假设—验证”的闭环。我把它压成一个可复用的四步流程每次遇到性能问题都走一遍。第一步固定现场记录 JDK 版本、启动参数、堆配置用jcmd pid VM.flags导出实际生效参数避免“以为设了其实没设”。第二步采集证据GC 日志、jstat采样、必要时堆 dump三样至少有两样。第三步提出单一假设并做对照实验比如只改收集器不改堆大小观察停顿分布变化。第四步把结论写回参数注释下次别人看到-XX:MaxGCPauseMillis200时知道为什么是 200 而不是 500。下面这个小脚本是我常用的现场快照跑一次把关键信息落到一个目录里事后复盘不用再问“当时参数是什么”#!/usr/bin/env bash # 采集 JVM 现场快照用法./jvm-snapshot.sh pid 输出目录 PID$1 OUT${2:-./jvm-snapshot-$(date %Y%m%d%H%M%S)} mkdir -p $OUT # 1. 实际生效的启动参数 jcmd $PID VM.flags $OUT/flags.txt 21 # 2. 命令行与系统属性 jcmd $PID VM.command_line $OUT/cmdline.txt 21 jinfo -sysprops $PID $OUT/sysprops.txt 21 # 3. 线程栈排查死锁和阻塞 jstack $PID $OUT/threads.txt 21 # 4. 堆直方图快速看谁占内存会触发一次轻量统计 jcmd $PID GC.class_histogram $OUT/histogram.txt 21 # 5. GC 采样 30 秒 jstat -gcutil $PID 1000 30 $OUT/gcutil.txt 21 echo 快照已写入 $OUTVM.flags输出的是最终生效值比翻启动脚本可靠GC.class_histogram不用 dump 全堆就能看到类实例数量和占用适合快速定位“是不是某个集合类爆了”jstack抓线程栈时建议连抓三次间隔几秒单次快照容易错过瞬时阻塞。这套脚本配合书里第 4 章的工具章节看命令行工具和可视化工具的分工就清楚了命令行适合线上快速取证JConsole、VisualVM、JMC 适合本地深挖和长期监控。一个容易忽略的细节是jcmd在 JDK 7 之后才逐步统一老版本部分子命令不存在脚本里每个命令都加了21把错误也收进文件跑完先看文件里有没有Exception再下结论。另外GC.class_histogram在堆很大时会有短暂停顿生产上尽量避开高峰或者改用jmap -histo:live的替代方案并接受它触发一次 Full GC 的代价。从那以后我每次接手一个新服务第一件事不是看代码而是先跑一遍这个快照脚本把 JDK 版本、收集器、堆配置、线程数摸清楚再谈优化。很多所谓的“性能问题”在参数和版本对不上的那一刻就已经注定了。希望这套拆法能帮到你把书里的原理变成自己手里的排查动作。本文还有配套的精品资源点击获取