ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深入理解Java虚拟机:内存模型、垃圾回收与调优实战指南

深入理解Java虚拟机:内存模型、垃圾回收与调优实战指南 很多人学 Java 学到一定阶段都会遇上这么一堵墙代码能写、框架会用但一碰到内存溢出、垃圾回收卡顿、接口频繁 Full GC 这类问题就感觉无从下手。这堵墙说到底就是对 Java 虚拟机JVM的理解不够。我刚工作那会儿也是这样直到啃完《深入理解Java虚拟机JVM高级特性与最佳实践第3版》再回看线上那些奇奇怪怪的问题才有一种“原来如此”的通透感。这本书不是让你背指令集也不是堆砌理论它是真的从 JVM 的内存模型、垃圾回收、类加载机制一路讲到实战调优和代码编译优化帮我把“JVM 到底在做什么”这件事彻底讲清楚了。这篇文章我会围绕这套书里最核心的内容结合我自己项目里踩过的坑把 JVM 的内存区域拆解、垃圾收集器选型、调优参数配置、类加载机制这些硬核知识用直白的话重新梳理一遍。不管你是准备面试、排查线上故障还是想把服务压出更好的性能这里面的东西都值得花时间看下去。1. 内容整体设计与思路拆解1.1 为什么说第 3 版是一本“能用起来”的 JVM 书最早接触 JVM 相关文章时我最大的感受是知识点碎这里一篇讲堆内存那里一篇讲垃圾回收看完觉得懂了真遇到问题还是不会查。第 3 版给我的感觉不一样它把“为什么这么设计”和“应该怎么用”绑在一起讲。比如讲内存区域它不是说堆有多大、栈有什么用就完了而是从 JVM 执行一个 Java 程序的全过程出发说明每一步数据放在哪里、谁负责分配、谁负责回收。书里把运行时数据区拆成程序计数器、虚拟机栈、本地方法栈、堆、方法区这几块每一块都对应一条清晰的职责线。看完之后我再去看线上服务的内存监控图就能大致判断出是哪一块区域在出问题而不是两眼一抹黑。更关键的是第 3 版把新版 JDK 带来的变化也写进去了。像 G1 收集器从实验状态转正、ZGC 的出现、字符串去重、CDS 归档这些特性书中都给出了对应版本和适用场景。这些点很实用因为网上很多文章写的还是 JDK 8 时代的旧方案你按那个思路去调新版本服务很容易调出反效果。1.2 从内存模型到垃圾回收主线为什么要这样安排很多初学者拿到这本书会先翻垃圾回收那几章觉得那块最“有意思”。但我觉得整本书的主线安排是有讲究的先讲内存区域的划分再讲对象是怎样被创建的、怎样判定生死的然后才引出各种垃圾收集器和回收算法。这条线本质上是在回答三个连环问题——内存在哪里、内存怎么被使用、内存如何被管理。我自己写代码的时候最直观的感受是“new 一个对象成本很低但对象多了内存就扛不住了”。只有理解了对象在堆里的分配规则以及栈上分配、TLAB线程本地分配缓冲这些优化手段你才会明白为什么推荐使用短生命周期的小对象、为什么对象的创建方式会影响 GC 压力。书里这种循序渐进的方式不是让你背知识点而是帮你建立一套排查问题的思维路径。1.3 这本书覆盖的三层能力原理、工具、调优如果把 JVM 学习分成三个层级我理解是这样第一层是“看得懂”知道 JVM 内部有哪些区域、类是怎么加载的第二层是“查得出”配合 JDK 自带的 jps、jstat、jmap、jstack 等工具能定位出问题大致出在哪第三层是“调得了”面对一个具体的性能瓶颈能够设计实验、调整参数、验证效果。第 3 版恰恰把这三层都覆盖了。它不只是讲规范还会教你怎么用工具去验证规范里的行为。比如讲内存区域时它会教你用 jstat 看各个区域的使用率变化讲垃圾回收时它会带你分析 GC 日志判断一次垃圾回收是否合理。说实话这种“原理加工具”配合的写法比单纯贴参数列表有用得多因为你在公司里排查问题的时候依赖的正是这套组合方式。2. 核心细节解析与实操要点2.1 运行时数据区每个程序员都该刻在脑子里的内存地图JVM 的内存划分是后续所有知识的基座。我见过不少同学面试时能背出“堆、栈、方法区”但问到“一个对象从创建到被垃圾回收都经历了哪些内存区域”就接不上来。这并不怪他们因为很多教程只讲了概念没讲对象的完整旅程。先说虚拟机栈这是线程私有的每调用一个方法就会压入一个栈帧。栈帧里保存着局部变量表、操作数栈、动态连接、方法返回地址这些信息。局部变量表里存的是基本类型和引用类型基本类型存值引用类型存地址。所以如果你的代码里有很多递归调用或者方法里定义了特别大的局部变量数组栈深度和栈帧大小都可能成为瓶颈。再讲堆这是所有线程共享的区域线程中创建的对象实例几乎都分配在这里。堆又被划分成新生代和老年代新生代里再细分出 Eden 区和两个 Survivor 区。为什么要这么折腾因为大多数对象都是“朝生夕灭”的把短命对象集中放在新生代用复制算法回收效率远高于在老年代里做标记整理。如果你理解了这一点就知道为什么书里反复强调调整新生代与老年代的比例、设置 Survivor 区大小都会直接影响 GC 的频次和停顿时间。方法区在 JDK 8 之后被彻底改成了元空间Metaspace把原本放在永久代里的类元数据移到了本地内存里。这带来的直接变化是以前常见的“PermGen space”异常不见了但如果你加载的类特别多比如频繁热部署、用了大量动态代理元空间同样可能膨胀到把机器内存吃满。我在实际项目里就遇到过类似情况后面在常见问题章节里会展开讲。还有本地方法栈和程序计数器。程序计数器是一块很小的内存用来记录当前线程执行的字节码行号分支、循环、跳转、异常恢复都依赖它。为什么这块区域不会出现内存溢出因为它占用的空间非常固定。我在排查问题时几乎不需要看程序计数器的数据但理解它的存在能让你读字节码相关的文章时顺畅得多。2.2 对象创建、内存分配与访问定位从new到真正可用很多人在学 JVM 之前以为new一个对象就是“在堆上分一块内存”其实里面的细节很有意思。JVM 在拿到一条new指令时先要去常量池里定位这个类的符号引用检查这个类有没有被加载、解析、初始化。如果没有先触发类加载过程。这一步常常被忽略但它是理解类加载机制和“对象创建是否触发初始化”问题的基础。类加载检查通过后JVM 就开始为对象分配堆内存了。虚拟机栈的局部变量表里有对象的引用堆里放着对象实例数据方法区里存着对象类型数据。说起来简单但如果严格按照“所有对象都在堆上分配”来理解就会忽视栈上分配、标量替换这种编译层面的优化。书里第 11 章讲编译器优化时提到HotSpot 虚拟机通过逃逸分析如果判断一个对象不会逃逸出方法就可能直接在栈上分配或者干脆把对象的字段拆成一个个标量。这就是为什么现代 JVM 上某些“new 出来的短命对象”并不会像想象中那么消耗堆内存。对象的内存布局分成三块对象头、实例数据、对齐填充。对象头里最重要的一块是 Mark Word它存储了对象的哈希码、GC 分代年龄、锁状态等信息。这也就是为什么我们能通过偏向锁、轻量级锁实现各种锁优化因为锁状态本身就是 Record 在对象头里的。再说访问定位。Java 程序通过栈上的引用去操作堆上的具体对象主流方式是使用句柄池还是直接指针HotSpot 默认用的是直接指针。直接指针的好处是速度快省去了一次定位句柄的开销Sun JDK 一直用这种方式。理解这个对普通业务开发可能没有直接影响但如果你想去深入理解 JNI、对象压缩指针等机制这是绕不开的基础。2.3 垃圾回收算法和收集器选型不是记住名字而是会选场景垃圾回收这块是 JVM 知识体系里最难啃、也最容易被问细节的部分。书里讲到引用计数算法和可达性分析算法时我印象最深的是“为什么不用引用计数”——它简单直观但解决不了循环引用问题两个对象互相引用但已经不可达引用计数永远不为零内存就泄漏了。HotSpot 主流的做法是用可达性分析从 GC Roots 出发一路标记能扫到的对象就是活着的。GC Roots 都有哪些虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI 引用的对象。所以在排查内存泄漏时如果你的对象被某个静态集合一直持有它就会顺着 GC Roots 被标记为存活永远也回收不掉。这种问题在实际线上非常常见尤其是一些生命周期很长的全局缓存一不小心就变成“隐性泄漏”。收集器的选择是这本书特别值得细读的部分。JDK 8 之后G1 开始成为主流它把堆划分成很多个大小相等的 Region从逻辑上让新生代和老年代不再是物理隔离。G1 最大的特点是可预测的停顿时间模型你可以通过-XX:MaxGCPauseMillis指定期望的 GC 停顿目标然后 G1 会尽量在你设定的时间窗内完成垃圾收集。但 G1 也不是万能的。如果你的服务内存很大比如上百 GB 的堆并且对延迟特别敏感那 ZGC 和 Shenandoah 这类几乎不停顿的收集器就更合适。ZGC 的核心思路是把标记和清理的耗时尽量挪到与应用线程并发执行并通过染色指针技术来管理对象状态。书里对 ZGC 的原理讲得比较细包括读屏障、染色指针、内存多重映射这些概念读的时候需要一点耐心但读懂了再看线上 ZGC 日志基本能判断出停顿是来自分配压力还是来自并发处理。具体到实际选型我的建议是如果你还在跑 JDK 8业务性能和延迟要求不算极端用 G1 并把-XX:MaxGCPauseMillis设置在 100~200 毫秒通常就能覆盖大多数场景如果你已经上了 JDK 11 甚至 17并且堆内存很大、延迟要求苛刻ZGC 很值得尝试。书里给了很多准则但真正的选型经验还是要靠自己在测试环境里压测对比。2.4 类加载机制打破“双亲委派”才是真正理解 JVM 的开始类加载这块很多文章都在讲双亲委派模型一个类加载器收到加载请求后先让父加载器去加载父加载器搞不定才轮到子加载器。这么设计的初衷很简单就是保证核心类库的类不会被随意替换比如你自己的java.lang.String不应该覆盖 JDK 自带的版本。但实际项目中我们经常会因为“打破双亲委派”而受益。最典型的就是 Tomcat。每个 Web 应用都希望有自己独立的类加载器这样多个应用部署在同一个容器里各自依赖的类库版本可以不同互不影响。Tomcat 的类加载器优先级是“自己先加载加载不到再让父加载器加载”这实际上就是为了隔离而打破常规模型。再看 JDK 9 之后的模块化系统JPMS类加载机制又发生了变化不再是简单的“父委托子”而是增加了模块层面的可读性和封装性控制。书里把这部分写得很细包括ClassLoader的源码核心方法、loadClass的双亲委派流程、findClass和defineClass的关系。我建议你把这类加载器的源码翻一翻结合书里的图解再去看热部署框架如 JSP 热加载、OSGi 这类的实现时思路会清晰得多。在排查线上问题时类加载相关的异常最常见的两种ClassNotFoundException和NoClassDefFoundError。前者是类找不到后者是类在编译期存在但在运行期加载失败比如静态初始化抛了异常。很多新手会把这两个混为一谈其实排查方向完全不同。书里对这部分的描述非常实战我遇到这类问题时直接照它的思路去检查依赖冲突和元数据区配置效率很高。3. 实操过程与核心环节实现3.1 快速准备环境从 JDK 版本到常用命令行工具讲再多理论不如自己动手搭一次实验环境。我的做法是用 Docker 起一个干净的 Linux 容器里面装 JDK 17因为第 3 版里很多新特性G1、ZGC、CDS 归档等在 JDK 11 以上的版本才更好验证。如果你机器上已经装了多个 JDK建议用update-alternatives或者 IDE 里的 JDK 配置把当前终端的java -version切到你目标版本。JDK 自带的命令行工具是排查问题的基础弹药建议你先把这几个用熟jps列出当前机器上的 JVM 进程相当于 Java 版的 ps。jstat监控 JVM 各个区域的内存用量和 GC 情况常用jstat -gc pid 1000每秒打印一次。jmap导出堆转储快照或者查询堆内对象统计信息。jstack打印线程快照排查死锁和线程阻塞时很有用。jcmd这是一个综合性工具很多 JVM 诊断操作都可以通过它完成。jhsdb在 JDK 9 之后逐步替代 jmap、jinfo 和 jstack 的部分功能适合对运行中进程做更深入的交互式排查。工具不要求多关键是知道什么场景用哪个。比如我判断一个服务是不是发生内存泄漏最常用的组合是jstat -gc观察老年代增长曲线如果老年代持续增长并且 Full GC 后回收效果很差再用jmap -dump抓一份堆快照进行离线分析。这个流程在书里也有完整演示跟着走一遍基本可以做到“定位到具体类和方法”。3.2 通过 GC 日志分析一次完整的垃圾回收过程看 GC 日志是理解垃圾回收最直接的方法但新手往往容易忽略它的价值。我现在还记得第一次打开 GC 日志时的反应一堆不认识的缩写和数字。其实只要拆开来看信息量是很丰富的。启动 JVM 时加上这些参数java -Xlog:gc*:filegc.log:time,uptime,level,tags -jar YourApp.jar在 JDK 9 之前你可能会更熟悉这样一堆老参数java -verbose:gc -Xloggc:gc.log -XX:PrintGCDetails -XX:PrintGCDateStamps -jar YourApp.jarJDK 9 开始采用统一的-Xlog语法推荐直接用新语法。日志里会有很多年轻代 GC 的片段比如[0.321s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 12M-2M(16M) 5.123ms这一行信息非常丰富时间是 0.321 秒GC 类型是 G1 的年轻代回收暂停堆内存从 12M 降到 2M总大小为 16M耗时 5.123 毫秒。如果这条日志出现在你没预料到的频率上或者耗时不断拉长那就说明你的对象分配速率太高或者新生代容量配置不合理。我实践中的建议是先在测试环境用一套固定的压测脚本跑 10 分钟期间采集 GC 日志然后重点看三组数——单次 GC 平均耗时、GC 频率、每次 GC 回收前后的堆占用差。如果每次回收后内存都回不到一个稳定的低水位说明存在对象滞留如果回收很频繁但每次回收量很少说明 Eden 区太小或者分配速率太高。这两类的调优方向完全不同前者要查引用后者要调比例。3.3 利用 jstat 与 jmap 排查一次线上内存异常分享一个我实际遇到的例子。某个订单导出服务平时运行很稳但一到月底导出量大的时候接口响应开始变慢随后个别节点直接 OOM。当时第一反应是堆太小于是把堆从 4G 调到 8G结果只撑了两天又出问题。这时候我才意识到不是堆不够而是有对象一直被持有GC 回收不掉。打开jstat -gc pid 5000持续观察看到老年代从 3G 一路涨到 7GFull GC 触发后老年代使用率几乎不下降这说明大量对象在 Full GC 后依然存活。接着用jmap -dump:live,formatb,fileheap.bin pid抓了一份当前存活对象的堆快照。把这个 heap.bin 加载到 MATMemory Analyzer Tool里通过 Leak Suspects 报告很快就锁定了一个统计报表类它内部有一个静态的HashMap会把每次导出任务中的查询条件、临时数据对象全部存进去而且只增不减说白了就是一个没有清理逻辑的缓存。排查到这里修复方案就很简单了要么把静态 Map 改成缓存框架并加上过期策略要么在任务结束以后主动释放引用。这个过程中书里“判断对象是否可被回收”的知识点给了我很大帮助——一个对象只要还能通过 GC Roots 链触达就不可能被回收而很多内存泄漏本质上就是让一堆无用对象长期“可达”罢了。3.4 常用 JVM 调优参数详解与配置示例JVM 调优参数并不是越多越好实际项目里有几个参数是真正的高频使用对象我在下面整理成表格方便你直接参考参数作用我常用配置说明-Xms初始堆大小与 -Xmx 保持一致避免 JVM 运行期频繁扩展堆先分配好减少波动-Xmx最大堆大小机器内存的 50%~70%留出空间给元空间、线程栈和堆外内存-Xmn新生代大小堆的 1/3 ~ 1/2太小导致对象提前升代太大则老年代空间不足-XX:MetaspaceSize元空间初始大小256m避免类元数据频繁触发扩容-XX:MaxMetaspaceSize元空间最大大小512m 或不设不设的话理论上可耗尽本地内存-XX:UseG1GC使用 G1 收集器JDK 9 默认开启需要显式设定或确认默认值-XX:MaxGCPauseMillisGC 预期停顿100ms 左右不是硬性指标G1 会尽量满足-XX:ParallelGCThreadsGC 并发线程数与 CPU 核数相关过大可能增加线程切换开销-XX:HeapDumpOnOutOfMemoryErrorOOM 时自动导出堆快照建议开启配合 -XX:HeapDumpPath 指定路径举一个比较典型的启动参数示例java -Xms4g -Xmx4g -Xmn2g \ -XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m \ -XX:UseG1GC -XX:MaxGCPauseMillis100 \ -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/ logs \ -Xlog:gc*:file/data/logs/gc.log:time,uptime,level,tags \ -jar OrderExportService.jar很多人问我要不要调-Xmn我的看法是用 G1 时新生代大小可以交给 JVM 自适应但当你对服务的对象分配特征非常了解觉得默认行为不够理想时手动指定新生代也是一种有效手段。比如对象大多短命且创建速度很快把新生代调大一点通常能明显降低 GC 频率。调完之后一定要用压测手段对比不对比参数改得再“合理”也只是心理安慰。3.5 压测环境下的对比验证怎么证明“调优有效”调优不是把参数改上去就完事你得用数据说话。我习惯用 wrk 或 JMH 做接口压测然后用jstat与 GC 日志对比调整前后的差异。重点看几项指标吞吐量TPS、平均响应时间、GC 总耗时、Full GC 次数、堆内存水位波动。我做过一次很典型的对比一个内部查询接口原来裸跑 TPS 大概是 800Full GC 每 5 分钟触发一次。第一次调优把堆从 2G 扩到 4GGC 次数降到每 15 分钟一次但单次 GC 耗时反而变长了接口 P99 延迟从 120ms 涨到 180ms。后来把新生代调大、减少进入老年代的对象数量Full GC 次数进一步下降P99 延迟才回到 110ms 左右。整个过程里如果不记录压测指标很容易觉得“堆越大越好”但实际上大堆意味着更大的 GC 压力必须配合回收行为一起看。书里有个观点我非常认同GC 调优的目标不是“消灭 GC”而是“让 GC 以可控的代价发生”。如果没有压测环境做支撑凭感觉调参数就是在给线上埋雷。4. 常见问题与排查技巧实录4.1 线上接口频繁 Full GC这个问题我见得太多了症状很统一接口偶发性卡顿监控面板上“老年代使用率”曲线快速上升GC 日志里 Full GC 的间隔越来越短。很多人第一反应是堆太小直接扩内存但扩完之后往往只是把问题往后拖延而不是解决源头。排查路径上我会先看jstat -gcutil pid 1000判断老年代增长是不是线性且无法回收。如果每次 Full GC 后老年代使用率能降到一个低点说明只是空间不够那么调整堆大小或释放内存是合理的如果老年代使用率降不下去基本可以确定有对象被长生命周期对象持有。这时候用 jmap 抓堆快照再在 MAT 里查 Dominator Tree重点找那些占用内存大、持有引用链长的对象基本都能定位到问题类。还有一种比较隐蔽的情况是线程池使用不当。如果你用Executors.newFixedThreadPool()创建线程池注意它底层是无界队列任务一多队列里的任务对象会全堆在老年代。这些任务对象必须等到任务执行完毕才会释放如果后续任务一直提交不出去老年代就会一直膨胀。书里虽然没有直接讲线程池但“对象存活判定”和“引用链分析”的知识点完全可以迁移到这类问题上来。4.2 元空间Metaspace持续增长引发 OOM这个坑我在热部署场景里踩过。当时是一个支持规则热更新的平台每次后台修改规则都会重新加载一批类结果跑上几天内存就用完了。用jstat -gc看堆内存一切正常但系统监控显示进程总内存不断上涨后来才发现是元空间的问题。JDK 8 之后类元数据放在本地内存中如果代码里不断生成新的类比如频繁创建动态代理类、CGLIB 代理类或者同一个类被不同类加载器反复加载元空间就会持续增长。排查时我用jcmd pid GC.class_histogram看到大量重复的代理类再结合业务代码确认是每次刷新规则都会 new 一个类加载器但旧的加载器和加载进来的类没法被回收最终把本地内存打爆。解决办法是复用类加载器或者把“热更新”改成更轻量的“配置刷新”而不是每个版本都重新加载类。如果实在避免不了动态生成类可以设置-XX:MaxMetaspaceSize来做上限保护至少在 OOM 之前能留下分析现场。4.3 死锁与线程阻塞问题怎么排查死锁不是 JVM 内存问题但它同样是线上故障的高频来源而且排查方式跟内存问题很不一样。症状通常是接口无响应、负载打满jstack能看到大量线程处于BLOCKED或WAITING状态。定位死锁最快的方法是先jps找到进程号再jstack pid thread.txt最后在 dump 文件里搜索Found one Java-level deadlock或Waiting to lock关键字。JVM 在 dump 线程快照时会把死锁的环直接打印出来告诉你哪个线程持有了哪把锁、哪个线程在等待哪把锁。如果发现线程并没死锁但大量线程阻塞在同一个锁上那通常不是锁竞争问题而是某个线程持锁时间过长比如在里面做了耗时很长的 IO 操作或远程调用。这时候要从业务代码找原因而不是盲目扩大线程池因为线程池扩大代表同锁竞争更激烈反而会加剧问题。书里讲并发和锁优化部分时反复强调减少锁持有时间、减少锁粒度、用读写锁替代互斥锁这些才是治本的手段。4.4 OOM 常见的几种类型与处理方案OOM 并不是一种单纯的问题它可以细分成几类每类的排查方向都不一样。根据自己的线上经验我通常把 OOM 分成下面这几种Java heap space堆内存耗尽。最常见的原因是对象泄漏或堆太小排查方向是堆快照分析。Metaspace元数据空间耗尽。常见于动态生成类或类加载器没有释放排查方向是类直方图和类加载器历史。unable to create new native thread操作系统线程数被耗尽。通常因为线程池无限创建、机器 ulimit 限制等排查方向是线程数和系统资源。Direct buffer memory堆外内存耗尽。常见于 NIO 使用不当没有释放 DirectByteBufferGC 时又没能及时触发回收。最后一种比较隐蔽而且堆内存监控看起来完全正常。因为我用的是 Netty 做过网关经常分配堆外内存如果 ByteBuf 没有正确 release就会一直占用 Direct Memory。排查方法是用jcmd pid VM.native_memory查看 JVM 内部各区域的内存使用再配合代码 review 找到未释放的缓冲区分配点。书里虽然没有专门讲 Netty但它对“直接内存”这块的原理讲得很清楚理解原理之后再去看 NIO 框架的问题思路会顺畅很多。4.5 排查技巧速查表与其每次遇到问题都网上搜不如整理一张自己的排查速查表。下面这张表里的工具和步骤是我在实际工作中用下来最顺手的组合症状首选工具关键排查点接口卡顿、Full GC 频繁jstat -gcutil老年代使用率、Full GC 次数与耗时内存泄漏jmap dump MATDominator Tree、Leak Suspects 报告线程死锁jstack搜索 deadlock、Waiting to lock元空间溢出jcmd GC.class_histogram重复类数量、类加载器实例数系统内存持续上涨但堆很低jcmd VM.native_memory查看 Direct Memory、Metaspace 等堆外区域实际过程中别被“工具应该是什么”限制住关键是养成“先量化、再定位、后修复”的习惯。任何一次故障只要你能用数据复现出异常特征就说明离根因不远了。4.6 一个让我印象深刻的“回车”引发的教训再分享一个很有意思的小坑。有一段时间某个服务的 GC 日志量特别大磁盘 IO 飙升运维找到我说是不是 GC 太频繁。我看完日志后发现GC 本身很正常问题出在日志配置上——日志文件里每条记录都带着完整的时间戳、线程 ID、标签信息而且没有按天滚动归档导致日志文件像滚雪球一样越来越大。这个问题的根源不在 JVM 参数而在运维侧。GC 日志在生产环境应该精确控制输出级别和文件轮转策略否则排查问题的最后反而会被日志本身拖垮。比如我现在的标准配置是-Xlog:gc*info:file/data/logs/gc.log:time,uptime,level,tags:filecount10,filesize50M这样 GC 日志最多保留 10 个文件每个文件最大 50M不会无限增长。书里讲了很多 JVM 内部机制但很多事情真的只有踩过坑才会意识到参数和工具不仅要会配还要懂得怎么收尾。5. 一些细节心得读这本书不如“用”这本书很多人读完技术书觉得会了但一上生产就原形毕露根因在于“懂原理”和“会排查”之间有很长一段距离。我建议你把《深入理解Java虚拟机》当成一本“案头参考书”而不是“一次性读本”。第一遍可以快速通读把内存区域、垃圾回收、类加载机制这几条主线串起来之后每次遇到线上故障就翻到对应章节带着问题去读效果远比从头背到尾好。比如你看到 GC 日志里的Pause Young (Normal)时去翻一遍 G1 回收的细节把 Region 分配、SATB 算法这些点重新过一遍很快就能把“印象”变成“手感”。书里的例子很用心很多都是基于真实场景提炼出来的多练几遍以后你会发现自己面对问题时的第一反应不再是“猜”而是“查”。我个人还有一个习惯每读完一个章节就顺手写一个复现实验比如用一个大 HashMap 去模拟内存抖动、用递归去触发栈溢出异常、用自定义类加载器去感受类加载器的父委托逻辑。自己亲手造出来的故障印象会深刻得多。这也是为什么我一直觉得这本书最大的价值不是帮你应付面试而是帮你建立一套属于自己排查 JVM 问题的坐标系。如果你能把这本书反复读透再结合实际项目的异常场景磨一遍JVM 这座山也就没那么可怕了。至少下次线上报警时你不会再只会重启了事而是能冷静地说一句“先看堆再看 GC 日志最后拿 MAT 分析。”
返回列表