目录
[一.什么是 GC?为什么它会让系统"卡死"?](#一.什么是 GC?为什么它会让系统“卡死”?)
[1.GC 的本质:自动清洁工](#1.GC 的本质:自动清洁工)
[2.为什么 GC 会导致卡顿?(Stop-The-World)](#2.为什么 GC 会导致卡顿?(Stop-The-World))
[三.实战诊断:如何判断是 GC 问题还是代码逻辑问题?](#三.实战诊断:如何判断是 GC 问题还是代码逻辑问题?)
[1.场景一:所有线程都卡顿,且 CPU 可能高也可能低](#1.场景一:所有线程都卡顿,且 CPU 可能高也可能低)
[2.场景二:单个线程 CPU 飙高,系统部分卡顿](#2.场景二:单个线程 CPU 飙高,系统部分卡顿)
[四.案例分析:一次真实的频繁 Full GC 排查](#四.案例分析:一次真实的频繁 Full GC 排查)
摘要 :很多开发者在面对线上系统卡顿时,常常听到运维或架构师提到"GC 太频繁"、"Full GC 导致 STW"、"内存泄漏"等术语,却难以将理论与实际现象联系起来。本文将通过通俗易懂的"房间清洁工"比喻,深入浅出地讲解什么是 GC、什么是内存泄漏,并结合
jstack、jstat等工具,手把手教你诊断因频繁 Full GC 导致的系统卡顿问题。无论你是刚入门的 Java 新手,还是希望夯实基础的中级开发,这篇文章都能帮你彻底搞懂 GC 那些事儿。
一.什么是 GC?为什么它会让系统"卡死"?
1.GC 的本质:自动清洁工
GC (Garbage Collection) ,即垃圾回收,是 Java 虚拟机(JVM)自动管理内存的核心机制。
我们可以把 JVM 的堆内存想象成一个房间 ,程序运行就是在这个房间里做手工:
- 创建对象 = 使用纸张和胶水制作手工。
- 对象废弃 = 制作完成后留下的废纸、空瓶子。
- GC (垃圾回收器) = 请来的自动清洁工。
在 C/C++ 中,程序员需要自己扫地(手动释放内存),一旦忘记就会出事。而在 Java 中,这个清洁工是自动工作的,它会定期巡视房间,把那些"没人要"的垃圾扔掉,腾出空间。
2.为什么 GC 会导致卡顿?(Stop-The-World)
虽然清洁工是好事,但它干活时有个规矩:为了安全,打扫时所有人必须停下手里的活。
- Minor GC (小扫):只清理年轻代(房间的一角),速度快,停顿时间短(几毫秒),用户几乎无感知。
- Full GC (大扫) :当房间角落堆满了,或者大物件(大对象)太多时,清洁工需要进行全场大扫除 。
- 此时,JVM 会触发 Stop-The-World (STW) 机制:暂停所有应用线程。
- 现象:用户点击按钮没反应、接口超时、页面转圈,甚至整个服务暂时"假死"。
如果清洁工太勤快 (频繁 Full GC),或者扫得太慢(单次耗时久),系统就会表现为持续的卡顿。
二.什么是内存泄漏?为什么它比"垃圾多"更可怕?
很多初学者容易混淆"垃圾太多"和"内存泄漏"。
1.核心区别
-
垃圾太多(正常现象) :
你做完手工,废纸确实没用了,清洁工也能识别出来。只是因为你做得太快,垃圾产生速度超过了清洁速度。
解决:换个大点的房间(增加堆内存),或者让清洁工勤快点(优化 GC 参数)。
-
内存泄漏(严重 Bug) :
你做完手工,把废纸随手扔在角落,但在你的"物品清单"(代码引用)上,依然标记着"此物有用,禁止丢弃" 。说白了就是该清理的垃圾没清,时间长了导致垃圾堆满房间。
- 清洁工进来一看:"哦,清单上说这个还要用,那我不能扔。"
- 结果:这些明明没用的废纸,永远无法被回收。
- 随着时间推移,房间会被这些"假宝贝"彻底填满,最终导致 OutOfMemoryError (OOM),程序崩溃。
2.常见的内存泄漏场景
- 静态集合类 :定义了一个
static List或static Map,不停地往里面加数据,却从来不移除。这个列表会伴随程序一生,里面的对象永远无法被回收。 - 未关闭的资源 :数据库连接、文件流、网络连接使用后忘记
close()。 - 监听器/回调:注册了监听器,但在组件销毁时忘记取消注册,导致对象被隐式引用持有。
结论 :无论是垃圾太多还是内存泄漏,最终结果都是**"没地方做新的手工了"**,但前者是"忙不过来",后者是"根本扫不掉"。
三.实战诊断:如何判断是 GC 问题还是代码逻辑问题?
当线上系统出现卡顿时,我们如何通过工具快速定位?
1.场景一:所有线程都卡顿,且 CPU 可能高也可能低
疑似原因:频繁 Full GC。
诊断步骤:
- 观察现象:系统整体响应极慢,所有接口超时。
使用 jstack 查看线程栈:
bash
1jstack <pid> > thread_dump.txt
- 关键特征 :打开
thread_dump.txt,你会发现大量线程处于WAITING或BLOCKED状态,且在等待锁。 - 核心线索 :你会看到一个特殊的线程
VM Thread处于RUNNABLE状态,或者看到很多线程都在等待GC task thread。 - 解读:这说明清洁工(GC 线程)正在疯狂大扫除,其他所有工人(业务线程)都被迫停工等待。
-
使用
jstat验证 GC 频率:bash1jstat -gcutil <pid> 1000- 观察
FGC(Full GC 次数)和FGCT(Full GC 总耗时)。 - 如果
FGC在短时间内疯狂增加(例如每秒几次),且YGC也很频繁,基本确认为 频繁 Full GC。
- 观察
2.场景二:单个线程 CPU 飙高,系统部分卡顿
疑似原因:死循环、复杂计算、无限递归。
诊断步骤:
-
使用
top找到高 CPU 进程 :bash1top -H -p <pid>找到占用 CPU 最高的线程 ID(例如
12345)。 -
转换进制 :将线程 ID 转为 16 进制(
12345->0x3039)。 -
使用
jstack定位代码 :bash1jstack <pid> | grep -A 20 0x3039- 关键特征 :你会看到具体的业务代码行号,比如
com.example.service.UserService.calculate(...)。 - 解读:这是某个工人在拼命干活(死循环),跟清洁工(GC)没关系。
- 关键特征 :你会看到具体的业务代码行号,比如
四.案例分析:一次真实的频繁 Full GC 排查
1.背景
某电商系统在促销期间,每隔几分钟就出现一次全系统卡顿,持续约 2-3 秒。监控显示 CPU 使用率周期性飙升。
2.排查过程
- 初步判断 :由于是周期性 且全局卡顿,怀疑是 GC 问题。
- 抓取现场 :
- 执行
jstat -gcutil <pid> 1000,发现FGC每分钟增加 10 次以上,且OGC(老年代使用率)一直维持在 98% 以上。 - 执行
jstack <pid>,发现大量线程阻塞,VM Thread活跃。
- 执行
- 根因分析 :
- 导出堆内存快照:
jmap -dump:format=b,file=heap.hprof <pid>。 - 使用 MAT (Memory Analyzer Tool) 分析快照。
- 发现 :一个用于缓存用户信息的
static Map大小达到了 1.5GB,且包含了大量过期的用户数据。 - 结论 :这是一个典型的内存泄漏。静态 Map 持有了大量无用对象的引用,导致老年代迅速填满,触发频繁 Full GC,但 GC 又无法回收这些对象,陷入死循环。
- 导出堆内存快照:
3.解决方案
- 临时止血:重启服务(清空静态变量)。
- 根本解决 :
- 修改代码,移除
static修饰符,改用带有过期策略的本地缓存(如 Caffeine)。 - 增加堆内存参数
-Xmx作为缓冲。 - 添加监控报警,当老年代使用率超过 85% 时提前预警。
- 修改代码,移除
五.总结与建议
表格
| 现象 | 可能原因 | 关键特征 (jstack) | 解决方向 |
|---|---|---|---|
| 全局卡顿,周期性强 | 频繁 Full GC | 大量线程等待,VM Thread 活跃 |
检查内存泄漏、增加堆内存、优化对象创建 |
| 单线程 CPU 100% | 死循环/复杂计算 | 特定业务线程栈很深,处于 RUNNABLE |
优化算法逻辑、修复死循环 |
| 系统缓慢,无报错 | 锁竞争/死锁 | 大量线程 BLOCKED,等待同一把锁 |
优化锁粒度、检查死锁 |
给开发者的建议:
- 规范编码 :避免滥用
static集合,及时关闭资源,防止内存泄漏。 - 合理配置 :根据业务量设置合适的堆内存大小(
-Xms,-Xmx),并选择合适的垃圾收集器(如 G1、ZGC)。 - 监控先行:部署 Prometheus + Grafana 或 APM 工具,实时监控 GC 频率和耗时,不要等到用户投诉才去查日志。
理解 GC 不仅仅是记住几个参数,更是理解 JVM 如何管理资源。希望这篇文章能帮你透过现象看本质,轻松应对线上的"卡顿"难题!
本文原创,转载请注明出处。如有疑问,欢迎在评论区留言讨论。