CPU 高和 GC 抖同时出现时,先证明谁是因谁是果?
CPU 飙高和 GC 频繁一起出现时,最怕的是一上来就站队。把时间线、线程归属、对象分配速率和回收效果对齐后,才能看清到底是 GC 在推高 CPU,还是业务热点先把 GC 带坏。
线上一旦出现 CPU 持续高位,同时 Young GC / Full GC 也开始明显抖动,团队很容易很快分成两派:有人盯着 GC 图怀疑 JVM 顶不住了,有人盯着热点线程认定是业务代码先出问题。
两边都不算错,真正麻烦的是:谁先下结论,谁就可能先走偏。CPU 和 GC 同时异常时,最耗时间的往往不是不会抓栈、不会看指标,而是把因果顺序看反了。
所以这篇文章只聚焦一件事:
当 CPU 高和 GC 抖一起出现时,先把谁领先、谁跟随、谁只是放大器这件事坐实。
这件事不需要玄学,通常把四组证据摆在一起就够了:时间线、线程归属、对象分配速率、以及 GC 回收效果。
先收住争论,再对齐证据
如果现场已经开始争“先查 CPU 还是先查 GC”,我会先把 5 组证据摆在一起:CPU 曲线、YGC / FGC 次数、Heap / Old 区趋势、线程归属,以及业务流量和线程池变化。CPU 和 GC 一起异常时,最容易错的不是命令没跑,而是因果顺序看反;时间线和线程归属没对齐,后面的分析很容易站错边。
先过一遍这张证据表,判断这次是 CPU 先坏,还是 GC 先拖。
| 你现在看到的现象 | 更像什么 | 下一步 |
|---|---|---|
| CPU 高和 GC 抖几乎同时出现,不确定谁先把系统拖坏 | 先把时间线和线程归属对齐 | 继续看本文 |
| CPU 明显高,但 GC 只是轻微波动 | 更像 CPU 热点先起来 | 先看 Java CPU 飙高怎么排查:一套线上定位顺序 |
| Full GC 已经频繁、Old 区回不下来 | 更像 GC / 内存专题 | 接 Full GC 频繁怎么办:先判断是不是内存泄漏 |
| Young GC 很密,但 Full GC 还不突出,吞吐先掉了 | 更像年轻代中间态 | 接 Young GC 很频繁但没 Full GC,为什么吞吐还是掉得厉害? |
| 服务更像全局冻结或停顿,而不是单纯 CPU 高 | 更像停顿 / Safe Point 这一类问题 | 接 JVM Safe Point 太久,为什么服务会像“卡死”一样? |
哪些时候先判因果,哪些时候直接下钻
如果你现在最大的分歧是“先查 CPU 还是先查 GC”,这篇更适合拿来收住站队:把时间线、线程归属和对象分配拉到同一张图上,看看到底是谁先把系统拖坏。
但有两种情况,不用在这里停太久:
- 如果你已经确认是 CPU 热点先起来,直接回
Java CPU 飙高怎么排查:一套线上定位顺序查线程和代码热点。 - 如果你已经确认是 GC 压力先起来,直接切到 GC / 内存那条线查对象分配和回收效果。
- 只有在 两边一起异常,团队还在争论先查哪边 时,这篇才最值钱。
这篇要解决的,不是把 CPU 和 GC 所有问题都讲完,而是把“谁是因、谁是果、谁只是放大器”这件事坐实。
一、为什么 CPU 和 GC 一起异常时最容易误判
因为它们本来就会互相放大。
1. GC 可以把 CPU 顶高
例如:
- 对象分配速率过快
- 老年代回收压力过大
- Full GC 频繁
- GC 线程持续抢占 CPU
这时你看到的是 CPU 高,但更早出现的其实可能是:
- 堆使用率升高
- YGC 频率变密
- Full GC 开始出现
- GC 停顿和回收耗时逐渐抬升
2. CPU 热点也可以把 GC 带坏
例如:
- 某段代码高频创建临时对象
- 异常风暴不断构造栈信息
- 重试逻辑疯狂序列化 / 反序列化
- 批量任务把大量对象一次性装进内存
这时先抬头的可能是业务线程 CPU,而 GC 只是跟着对象分配压力一起恶化。
3. 两者也可能都不是起点,而是共同结果
例如:
- 缓存大面积失效,回源后请求链路变重
- 下游变慢,线程堆积,重试增加
- 数据库慢,单次任务拉长,队列堆积,内存和 CPU 一起抖
也就是说,CPU 和 GC 并不一定是彼此的因果,也可能是被更上游的问题一起推高。
所以这类问题最怕的不是复杂,而是你一开始就站错边。
二、第一步别急着抓栈,先把时间线立住
判断因果,第一件事不是命令,而是时间顺序。
你真正要回答的是:
- CPU 先开始抬高,还是 GC 指标先变差?
- 它们是同一时间一起抬头,还是一个领先另一个几分钟?
- 在它们抬头之前,业务流量、接口 RT、线程池、数据库、缓存有没有先变化?
更建议一起看的指标
- 实例 CPU 使用率
- load / 上下文切换
- YGC 次数、FGC 次数、GC pause
- Heap / Old 区趋势
- 对象分配速率
- QPS、RT、错误率
- 线程池 active、queue、reject
- 数据库连接池 active、pending
- 下游调用 RT、超时率
三种最常见的时间形态
1. GC 先抖,CPU 后抬
更像:
- 内存回收压力先变大
- GC 线程开始持续抢 CPU
- 业务线程随后被拖慢
这时更优先怀疑:
- 对象分配风暴
- 老年代回不下来
- Full GC 频繁
- 本地缓存或长生命周期对象膨胀
2. CPU 先抬,GC 后跟着恶化
更像:
- 某个热点代码路径先把 CPU 顶高
- 同时制造大量对象
- GC 随着分配速率一起被放大
这时更优先怀疑:
- 死循环或空转
- 高频重试
- 日志 / 异常风暴
- 大对象转换、序列化、聚合
3. 两者几乎同时抬头
更像:
- 更上游有共同触发因素
- 比如流量暴涨、缓存失效、批任务启动、最近一次发布引入了更重链路
这时不能只看 JVM,必须把业务链路一起拉进来。
三、第二步:先看“是谁在吃 CPU”,再看“GC 到底回得动吗”
时间线立住后,下一步要把两个方向都拉出证据。
1. 先看 CPU 到底主要被谁吃掉
不要只看实例总 CPU,要继续分层:
- 是 GC 线程占比明显高
- 还是业务线程占比更高
- 是少数热点线程持续高
- 还是大量线程一起抬升
常见做法仍然是:
top -Hp <pid>
pidstat -t -p <pid> 1 5
jstack <pid>
你这一步真正想回答的是:
- 高 CPU 线程是不是主要落在
GC Thread、VM Thread这类 JVM 线程上 - 还是更多落在业务线程、线程池工作线程、Netty / Tomcat 线程上
- 多次抓栈后,热点栈位置是否稳定重复
如果主要是 GC 线程高
更像:
- 回收压力真的已经在抢 CPU
- GC 至少不是“完全无关的旁观者”
但注意,这也不等于问题就一定是 JVM 参数。GC 线程高,只能说明 GC 很忙,还不能说明 为什么这么忙。
如果主要是业务线程高
更像:
- 业务代码本身有计算热点
- 或者它在疯狂制造对象
- GC 是被业务路径带起来的
这种情况下,先直接把锅扣给 GC,通常会偏。
2. 再看 GC 到底是“频繁”还是“无效”
判断 GC 时,不能只看次数,更要看回收效果。
例如:
jstat -gcutil <pid> 1000 10
你重点要看:
- YGC / FGC 是否突然增多
- Full GC 后 Old 区是否明显回落
- 回落之后是不是很快又顶上去
- 内存基线是不是在持续上移
如果 GC 频繁,但回收后能明显降下来
更像:
- 对象制造太快
- 任务窗口制造内存峰值
- CPU 热点和对象分配风暴一起出现
这类问题未必是传统泄漏,更常见的是任务执行方式或热点路径退化。
如果 GC 频繁,而且回收后也降不下来
更像:
- 长生命周期对象堆积
- 缓存膨胀
- ThreadLocal、静态集合或引用链没释放
- Full GC 已经在做“低效挣扎”
这时 GC 更可能是受害者,但它已经成了 CPU 高的重要放大器。
四、第三步:把问题归到 3 类典型因果模型里
把线程和回收效果都看过后,现场通常可以收敛到下面三类之一。
模型一:GC 是主因,CPU 是结果
典型特征
- GC 指标先恶化
- GC 线程 CPU 占比明显
- Full GC 或 YGC 增长非常快
- 回收后内存下降有限,或者下降后很快再冲高
- 业务线程不一定特别热,但整体 RT、吞吐开始变差
常见真实根因
- 老年代对象回不下来
- 缓存 / 集合长期膨胀
- 批量任务制造了巨大的对象峰值
- Heap 配置和业务模型不匹配
这时更该继续往哪里查
模型二:业务 CPU 热点是主因,GC 是结果
典型特征
- 业务线程先热起来
- 热点线程栈稳定落在业务方法、序列化、计算逻辑、异常构造或重试逻辑上
- YGC 会跟着变密,但 Full GC 未必先抬
- 内存回收仍然有效,只是分配速率太快
常见真实根因
- 大量对象临时创建
- 高频 JSON 转换、Map / List 复制
- 批量聚合、报表、导出路径过重
- 错误重试风暴
- 日志和异常对象创建过于激进
这时更该继续往哪里查
模型三:更上游的等待链同时把 CPU 和 GC 带坏
典型特征
- CPU、GC、RT、线程池、连接池一起抬头
- 单看 JVM 指标无法解释全部现象
- 热点线程既有业务处理,也有等待和重试痕迹
- 问题常和流量高峰、缓存失效、数据库慢、发布变更同步出现
常见真实根因
- 缓存失效后回源链路变重
- 下游超时导致重试和补偿放大
- 数据库变慢导致任务堆积,线程、对象和 CPU 一起被拖高
- 线程池堆积后任务集中释放
这时更该继续往哪里查
五、一个更稳的实战排查顺序
如果线上现在就碰到了“CPU 高 + GC 抖”,我更建议按这个顺序走。
第 1 步:先看时间线
- CPU 和 GC 谁先抬头
- RT、QPS、错误率是否同步变化
- 最近是否有发布、批任务、流量、缓存或数据库事件
第 2 步:拆开 CPU 归属
top -Hppidstat -t- 多次
jstack - 分清是 GC 线程忙,还是业务线程忙
第 3 步:看 GC 回收效果
- YGC / FGC 次数
- Full GC 后 Old 是否下降
- 内存基线是否持续抬高
第 4 步:看对象分配和业务链路
- 是否有明显对象制造热点
- 是否存在批量任务、报表、重试、异常风暴
- 线程池和连接池是否也在同一时间窗变差
第 5 步:最后再决定主线
- 更像 GC 先恶化,就走 JVM / 内存方向
- 更像业务线程先热,就走 CPU / 代码热点方向
- 更像共同结果,就回到接口、线程池、数据库、缓存链路上
这一步的重点不是“找到唯一理论”,而是先排除最容易走偏的路线。
六、一个典型例子:为什么“先看 GC”会误判
假设某个实例在高峰期突然出现:
- CPU 从 35% 升到 92%
- YGC 次数明显增多
- 接口 RT 抖动
- 大家第一反应是 GC 出问题
如果你只看 GC 图,很容易直接去调堆、调参数。
但继续往下看:
top -Hp发现主要高 CPU 线程不是 GC 线程,而是业务线程池- 连续三次
jstack发现热点栈长期落在serializeAndBuildResponse jstat看到 YGC 变密,但 Full GC 并不突出,而且回收后内存能明显回落- 同一时间窗内某个新接口开始返回超大响应,导致对象创建和序列化成本一起飙升
这时真正的链路就很清楚:
- 先是业务线程 CPU 高
- 同时制造大量临时对象
- YGC 频率被带高
- 大家看见 GC 抖,误以为 JVM 是起点
问题根因其实不在 GC,而在接口返回模型和对象制造成本。
七、关键误判:这类问题最容易在哪些地方走偏
误判 1:看到 GC 次数高,就默认 GC 是根因
GC 次数高只能说明 GC 很忙,不能说明 是谁先把它搞忙的。
误判 2:看到 CPU 高,就完全忽略内存趋势
很多业务 CPU 热点最后会演变成对象分配风暴,如果你不看 Old 区和回收效果,就很容易漏掉“CPU 正在把 GC 一起带坏”。
误判 3:只抓一次 jstack 就下结论
单次线程栈只能说明瞬间位置,不能证明持续热点。CPU 和 GC 都是动态问题,证据必须有时间维度。
误判 4:把 JVM 问题和线程池、数据库、下游等待完全拆开看
真实线上里,它们经常就是一条链。线程池堆积、下游超时、数据库慢,都可能让对象积压、重试增加,最后把 CPU 和 GC 一起推高。
误判 5:先调参数,再去定因果
参数调整有时能止血,但如果因果还没分清,后面很容易把现场洗掉。
八、CPU 高和 GC 抖一起出现时,最容易问偏的几个问题
1. 这种现场到底先抓 CPU 还是先抓 GC?
先抓能还原因果的证据,不是先选阵营。通常第一轮就把 CPU 曲线、GC 次数、热点线程、Heap / Old 趋势和业务时间线放在同一个时间窗里。
2. GC 线程 CPU 很高,能不能直接认定 GC 是根因?
还不能。它只能说明 GC 已经非常忙,接下来还得继续问:是分配速率突然上来了,还是老年代对象回不下去,或者更上游链路把 JVM 一起拖进来了。
3. 业务线程 CPU 很高,是不是就可以先不看 GC?
也不行。很多业务热点会顺手制造大量短命对象,GC 很可能只是慢半拍跟着恶化。只看 CPU 热点,不看回收效果,后面还是容易漏掉真正的放大点。
4. YGC 很密但 Full GC 不多,这更像什么?
更像短命对象制造过快,常见在线上序列化、批量聚合、异常风暴、重试放大这些路径。这个时候比调参数更重要的,是先把对象是谁造出来的找出来。
5. 什么时候 Heap Dump 才值得优先抓?
当你已经看到 Full GC 后 Old 区回不下来、内存基线持续上移,或者怀疑长生命周期对象在堆里堆住了,再往 Heap Dump 和 MAT 方向走,收益通常更高。
如果第一轮证据已经开始收敛
可以直接顺着最像的方向继续下钻:
- 热点线程已经很稳定,业务栈反复出现:看
Java CPU 飙高怎么排查:一套线上定位顺序 - 年轻代回收越来越密,吞吐先被切碎:看
Young GC 很频繁但没 Full GC,为什么吞吐还是掉得厉害? - Full GC 已经频繁,回收后 Old 区还是压不下去:看
Full GC 频繁怎么办:先判断是不是内存泄漏 - Old 区没满却已经开始反复 Full GC:看
老年代没有打满却频繁 Full GC,通常意味着什么? - 更像统一停顿、冻结或 Safe Point 问题:看
JVM Safe Point 太久,为什么服务会像“卡死”一样? - 你需要回到 JVM 工具证据层重新取样:看
线上问题排查时,jstack、jmap、jstat 分别怎么看
九、如果你准备继续补证据
如果你需要把这次判断继续往下拆,下面这些文章更像下一步该补哪一类证据,而不是单纯给你一串目录。
这篇更接近哪类问题
它属于 JVM 与 Java 性能排查,但重点不是单看 JVM 参数,而是先把 CPU、GC 和业务链路的因果顺序钉住。
想继续把 JVM 这一段查细
- 《Java CPU 飙高怎么排查:一套线上定位顺序》
- 《Full GC 频繁怎么办:先判断是不是内存泄漏》
- 《Java 内存持续上涨但没有 OOM,通常意味着什么?》
- 《线上问题排查时,jstack、jmap、jstat 分别怎么看》
如果你开始怀疑 JVM 只是其中一段
手里证据不同,下一步也不同
- 先把这篇里的 时间线 -> 线程归属 -> 回收效果 -> 业务链路 走完,别急着先站 CPU 还是 GC。
- 如果证据更像 GC 先恶化,就接着看《Full GC 频繁怎么办:先判断是不是内存泄漏》和《Java 内存持续上涨但没有 OOM,通常意味着什么?》。
- 如果证据更像业务线程先热,就继续查《Java CPU 飙高怎么排查:一套线上定位顺序》。
- 如果你已经看出 JVM 只是链路中间一段,就把线程池、数据库、接口慢这些方向一起带回来核对。
十、最后总结:先把因果顺序钉住,再谈优化动作
CPU 和 GC 一起抖时,最浪费时间的不是不会排查,而是太早选边。
真正能帮现场收敛的,往往只是几件朴素的事:把时间线对齐、把热点线程和 GC 线程拆开看、确认回收到底有没有效果,再把业务流量和上下游变化拉回来一起解释。
先把谁领先、谁跟随、谁只是放大器说清楚,再决定该往 JVM、代码热点,还是更上游调用链继续追。
这一步做扎实以后,CPU 高和 GC 抖就不会再只是两张吓人的图,而会变成一条可验证、可复盘的因果链。