JVM 内存与 GC 调优¶
一句话:JVM 调优是为线上 Full GC 事故"找凶手、定原因、改参数"的系统化能力,区别于只会背理论的"纸面调优"。
概念¶
JVM 是 Java 跨平台的基石,它的核心问题是内存如何分配与垃圾如何回收。生产环境最常见的稳定性故障不是代码 Bug,而是 Full GC 频繁导致 STW(Stop-The-World)、接口大面积超时。
理解 JVM 的关键在于把"运行时数据区"和"GC 算法"对应起来:哪些区域线程私有、哪些共享;哪些区域会触发 GC、哪些会抛 OOM;不同 GC 器的堆划分与回收策略差异如何影响停顿时间。资深后端工程师的价值,在于能用 jstat、jmap、MAT 在事故现场快速定位是"大对象进老年代""ThreadLocal 泄漏"还是"内存泄漏",而不是只会重启。
原理¶
运行时数据区¶
| 区域 | 线程私有? | 存储内容 | 异常 |
|---|---|---|---|
| 程序计数器 (PC) | 是 | 当前线程执行字节码行号 | 唯一不会 OOM 的区域 |
| 虚拟机栈 | 是 | 栈帧(局部变量、操作数栈) | StackOverflowError / OOM |
| 本地方法栈 | 是 | Native 方法调用 | StackOverflowError / OOM |
| 堆 (Heap) | 否(共享) | 对象实例、数组 | OOM(最常见) |
| 方法区/元空间 | 否(共享) | 类元信息、常量池、静态变量 | OOM: Metaspace |
| 直接内存 | 否(共享) | NIO Buffer 等 | OOM: Direct buffer |
线程私有的是:程序计数器、虚拟机栈、本地方法栈;共享的是:堆、方法区(元空间)、直接内存。这是高频考点。
对象的内存流转¶
新对象先分配在 Eden 区;Minor GC 后存活对象进 Survivor(From/To 来回复制,默认经历 15 次 -XX:MaxTenuringThreshold 进老年代);大对象直接进老年代(-XX:PretenureSizeThreshold)。老年代满了触发 Full GC(同时回收新生代+老年代+元空间),停顿时间长。
GC 器选型与调优¶
| GC 器 | 算法 | 停顿特点 | 适用场景 |
|---|---|---|---|
| Serial | 复制/标记整理 | 长停顿 | 单核、小堆、客户端 |
| Parallel (PS+PO) | 并行 | 吞吐优先,停顿中等 | 批处理、离线计算 |
| CMS | 标记清除 | 低停顿,但有碎片、并发模式失败 | JDK8 前低延迟(已废弃) |
| G1 | 分区+标记复制 | 可控停顿(-XX:MaxGCPauseMillis) |
大堆(4~32G)通用首选 |
| ZGC | 染色指针+读屏障 | 亚毫秒~毫秒级停顿,TB 级堆 | 超低延迟、超大堆、JDK15+ 生产可用 |
G1 把堆划分为多个 Region(Eden/Survivor/Old/Humongous),优先回收垃圾最多的 Region(Garbage First),适合追求可控停顿的大堆场景,是 JDK 9 起的默认 GC。
ZGC 用染色指针和读屏障实现并发整理,停顿时间不随堆大小增长(< 1ms ~ 10ms),适合对延迟极度敏感、堆极大(几十~几百 G)的场景。JDK 15 起生产可用,JDK 21 起进一步成熟。
选型经验:堆 4~32G、追求吞吐与停顿平衡选 G1;堆超大或对 P99 延迟极其敏感选 ZGC。
Full GC 排查工具链¶
| 工具 | 用途 |
|---|---|
jstat -gcutil <pid> 1s |
实时看各代占用、GC 次数与耗时,判断是 Young 还是 Full GC 频繁 |
jmap -heap <pid> |
查堆配置与各代使用 |
jmap -histo:live <pid> |
触发 GC 并按对象大小排序,快速锁定"大户类" |
jmap -dump:format=b,file=heap.hprof <pid> |
导出堆 dump |
| MAT | 分析 hprof,定位 GC Root、支配树、内存泄漏嫌疑 |
Arthas dashboard/heapdump |
在线诊断,适合不便重启的生产环境 |
实战要点¶
结合报告中的"线上 Full GC 频繁(大对象进老年代 / ThreadLocal 泄漏)"事故排查经验:
-
大对象进老年代是最常见的 Full GC 元凶之一:一次接口返回上千条大对象、或大数组/长 String 直接分配,绕过 Eden 进老年代,频繁触发 Full GC。排查先用
jstat -gcutil看 Old 区占用是否持续涨且 Full GC 次数飙升,再用jmap -histo:live锁定大户对象。修复方向是分页、流式处理、避免一次性构造大集合。 -
ThreadLocal 泄漏是隐蔽的元凶:
ThreadLocal的 value 被 Entry 的弱引用 key 间接持有,但 value 是强引用,线程池中线程长期存活,若用完不remove(),value 永久驻留。在线程池场景下表现为 Old 区缓慢上涨、Full GC 后不回收。修复是在try-finally里必调ThreadLocal.remove(),并用 MAT 的 "Leak Suspects" 看ThreadLocalMap持有大量无法回收对象。 -
G1 调优三参数:
-XX:MaxGCPauseMillis(目标停顿,默认 200ms)、-XX:G1HeapRegionSize(Region 大小)、-XX:InitiatingHeapOccupancyPercent(IHOP,触发并发标记的阈值,默认 45%)。堆大、Mixed GC 跟不上时可调低 IHOP。 -
ZGC 上 ZGC:报告通过者中有人 JVM 调优突出(G1/ZGC)。切换 ZGC 只需
-XX:+UseZGC,配合-XX:SoftMaxHeapSize控制软上限。低延迟接口(如交易下单)切 ZGC 后 P99 停顿显著改善,但要验证应用是否依赖 CMS/G1 特有行为。 -
生产别用
jmap -dump全量 dump 长期挂着:全量 dump 会 STW 且占磁盘,应在报警时按预案 dump 一次后立即恢复,或用-XX:+HeapDumpOnOutOfMemoryError让 OOM 时自动 dump。 -
类加载泄漏看元空间:动态生成类(CGLIB、Groovy 脚本、热部署)若 ClassLoader 无法卸载,Metaspace 持续涨最终 OOM。排查看 Metaspace 指标与
jmap -clstats。
本节相关题目¶
| 难度 | 题目 | 链接 |
|---|---|---|
| 基础 | JVM 运行时数据区、哪些线程私有 | → 题库 |
| 进阶 | G1 vs ZGC 算法与场景、何时选 ZGC | → 题库 |
| 进阶 | 频繁 Full GC 完整排查路径 | → 题库 |
| 深度 | ThreadLocal 内存泄漏线上事故全过程 | → 题库 |