跳转至

JVM 内存与 GC 调优

一句话:JVM 调优是为线上 Full GC 事故"找凶手、定原因、改参数"的系统化能力,区别于只会背理论的"纸面调优"。

概念

JVM 是 Java 跨平台的基石,它的核心问题是内存如何分配垃圾如何回收。生产环境最常见的稳定性故障不是代码 Bug,而是 Full GC 频繁导致 STW(Stop-The-World)、接口大面积超时

理解 JVM 的关键在于把"运行时数据区"和"GC 算法"对应起来:哪些区域线程私有、哪些共享;哪些区域会触发 GC、哪些会抛 OOM;不同 GC 器的堆划分与回收策略差异如何影响停顿时间。资深后端工程师的价值,在于能用 jstatjmapMAT 在事故现场快速定位是"大对象进老年代""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 泄漏)"事故排查经验:

  1. 大对象进老年代是最常见的 Full GC 元凶之一:一次接口返回上千条大对象、或大数组/长 String 直接分配,绕过 Eden 进老年代,频繁触发 Full GC。排查先用 jstat -gcutil 看 Old 区占用是否持续涨且 Full GC 次数飙升,再用 jmap -histo:live 锁定大户对象。修复方向是分页、流式处理、避免一次性构造大集合。

  2. ThreadLocal 泄漏是隐蔽的元凶ThreadLocal 的 value 被 Entry 的弱引用 key 间接持有,但 value 是强引用,线程池中线程长期存活,若用完不 remove(),value 永久驻留。在线程池场景下表现为 Old 区缓慢上涨、Full GC 后不回收。修复是try-finally 里必调 ThreadLocal.remove(),并用 MAT 的 "Leak Suspects" 看 ThreadLocalMap 持有大量无法回收对象。

  3. G1 调优三参数-XX:MaxGCPauseMillis(目标停顿,默认 200ms)、-XX:G1HeapRegionSize(Region 大小)、-XX:InitiatingHeapOccupancyPercent(IHOP,触发并发标记的阈值,默认 45%)。堆大、Mixed GC 跟不上时可调低 IHOP。

  4. ZGC 上 ZGC:报告通过者中有人 JVM 调优突出(G1/ZGC)。切换 ZGC 只需 -XX:+UseZGC,配合 -XX:SoftMaxHeapSize 控制软上限。低延迟接口(如交易下单)切 ZGC 后 P99 停顿显著改善,但要验证应用是否依赖 CMS/G1 特有行为。

  5. 生产别用 jmap -dump 全量 dump 长期挂着:全量 dump 会 STW 且占磁盘,应在报警时按预案 dump 一次后立即恢复,或用 -XX:+HeapDumpOnOutOfMemoryError 让 OOM 时自动 dump。

  6. 类加载泄漏看元空间:动态生成类(CGLIB、Groovy 脚本、热部署)若 ClassLoader 无法卸载,Metaspace 持续涨最终 OOM。排查看 Metaspace 指标与 jmap -clstats

本节相关题目

难度 题目 链接
基础 JVM 运行时数据区、哪些线程私有 → 题库
进阶 G1 vs ZGC 算法与场景、何时选 ZGC → 题库
进阶 频繁 Full GC 完整排查路径 → 题库
深度 ThreadLocal 内存泄漏线上事故全过程 → 题库