关键要识别gc日志中的真实行为模式:如“98%时间gc只回收2%空间”预示gc overhead异常;老年代几乎不下降提示内存泄漏;单次停顿超1秒伴concurrent mode failure说明碎片严重;需结合jstat验证、堆镜像定位根因,再按场景精准调参。

遇到 JVM GC 日志中反复出现异常信号,不是简单加内存就能解决的。关键要识别日志里暴露的真实行为模式——比如频繁回收却收效甚微、老年代几乎不下降、停顿时间突增等,这些才是调优的起点。
看懂三类高频报错信号
GC 日志不是用来“读完”,而是用来“比对”的。重点关注以下三类典型模式:
-
“98%时间GC,只回收2%空间”:这是
java.lang.OutOfMemoryError: GC overhead limit exceeded的直接前兆。日志里会密集出现G1 Evacuation Pause (young)或PSYoungGen,且每次回收后 Eden 区释放量极小(如 2048M→2040M),说明对象在新生代反复存活、快速晋升 -
Full GC 间隔越来越短,老年代占用率纹丝不动:例如连续几次
Full GC (Metadata GC Threshold)或Full GC (Ergonomics),但Old区从 1800M→1795M→1792M→1790M,降幅不足 1%,大概率存在内存泄漏或大对象长期驻留 -
单次 GC 停顿超长(>1s)且伴随
Concurrent Mode Failure或Evacuation Failure:多见于 G1,说明堆碎片严重或 Humongous 对象过多;若日志中频繁出现to-space exhausted,则 Survivor 区太小或对象年龄阈值不合理
用 jstat 快速验证问题类型
不用等日志堆积,用一条命令就能判断当前 GC 健康度:
- 执行
jstat -gcutil <pid> 2000 5</pid>(每2秒输出一次,共5次),重点看:
FGC(Full GC 次数)是否持续上升;
O(Old 区使用率)是否 >85% 且不回落;
YGCT/FGCT 占总运行时间比例是否超过 10% - 如果
EC(Eden 使用率)始终 >95% 且YGC频繁(如每秒 2–3 次),说明新生代太小或对象创建速率过高,需检查业务逻辑是否在循环中新建大对象 - 若
M(Metaspace)使用率持续上涨并触发 Full GC,加参数-XX:MaxMetaspaceSize=512m并观察是否缓解;未缓解则可能是动态类生成(如 Groovy 脚本、CGLIB 代理爆炸)
抓堆镜像定位“真凶”对象
日志只说“哪里疼”,堆镜像才告诉你“为什么疼”。别等 OOM 才 dump:
- 在 GC 高频期主动执行:
jmap -dump:format=b,file=heap-$(date +%s).hprof <pid></pid> - 用 VisualVM 或 JProfiler 打开后,按 Retained Heap 排序,重点关注前三名类:
不是实例数最多那个危险,而是“谁一直强引用着它”——比如一个ConcurrentHashMap实例本身只占 100KB,但如果被static final缓存持有,它拖住的 value 可能是几百 MB - 点开可疑类的 Reference Chain,逐层往上查:
常见泄漏源头包括:静态 Map 未设过期策略、ThreadLocal 的 value 是大对象且未调用remove()、监听器注册后没反注册、InputStream/Socket 未 close 导致关联缓冲区滞留
参数调优要匹配场景,不是堆越大越好
盲目加大 -Xmx 往往让问题更隐蔽。优先调整回收行为:
- 新生代太小 → 频繁 Minor GC:设
-Xmn512m(建议为堆的 1/4~1/3),配合-XX:SurvivorRatio=8,避免对象因 Survivor 满而提前晋升 - 老年代碎片化 → G1 回收退化:启用
-XX:+UseG1GC -XX:MaxGCPauseMillis=200,并加-XX:G1HeapRegionSize=2M(若存在大量 1–2MB 对象) - 低延迟敏感型服务(如网关、实时风控)→ 尝试 ZGC:
-XX:+UseZGC -XX:ZCollectionInterval=5s(Java 11+,需确认系统支持mmap大页) - 临时诊断可关掉限制:
-XX:-UseGCOverheadLimit(仅限排查期,必须配合 GC 日志观察,否则掩盖泄漏)











