线上告警突然提示 cpu 使用率达到 90%,接口延迟也开始上升。此时你会先重启服务,还是立刻执行 jstack?
重启也许能暂时止损,但会破坏最有价值的故障现场;盲目抓线程栈,又可能面对几百个线程无从下手。java 应用 cpu 飙高的排查关键不是记住某一条命令,而是建立一条稳定的证据链:确认指标 → 找到进程 → 找到线程 → 定位代码 → 判断根因。
本文整理一套可以直接用于 linux 服务器的排查流程,并补充容器、gc 和 arthas 等常见场景。
一、先判断:真的是 cpu 问题吗?
收到告警后,不要马上认定“java 代码有死循环”。先确认三个问题。
1. cpu 是瞬时尖峰还是持续升高?
发布、类加载、jit 编译和流量突增都可能造成短时尖峰。若 cpu 很快恢复,通常不必按持续故障处理;如果连续数分钟处于高位,并伴随响应时间上升、超时或吞吐下降,就需要继续定位。
top uptime mpstat -p all 1
top 用于观察整体 cpu,mpstat 可以看到每个逻辑核的使用情况。需要特别区分:
- 单核接近 100%:可能是某个线程死循环或单线程密集计算;
- 多核整体升高:可能是流量增长、线程数量过多、频繁 gc 或批量计算;
%wa较高:cpu 实际在等待 i/o,重点应转向磁盘或存储;- load average 很高但 cpu 不高:可能存在不可中断 i/o、锁等待或大量排队任务。
2. 是宿主机繁忙,还是 java 进程繁忙?
一台机器上通常不止一个进程。先通过 top 或 pidstat 找到真正消耗 cpu 的进程:
top -c pidstat -u 1 5
假设发现 pid 为 12345 的 java 进程长期占用大量 cpu,再进入线程级排查。
3. 容器是否被 cpu 限流?
在 kubernetes 中,即使监控看到 cpu 没有打满,请求也可能因为配置了较低的 cpu limit 而被节流。此时要结合 pod 的 request、limit、节点负载以及 cpu throttling 指标判断。
kubectl top pod <pod-name> -n <namespace> kubectl describe pod <pod-name> -n <namespace>
如果应用频繁触碰 cpu limit,应该先分清是资源额度不合理,还是代码计算量异常,而不是直接增加线程数。
二、从高 cpu 进程定位到高 cpu 线程
java 进程内部可能有数百个线程,下一步要找到真正消耗 cpu 的线程。
top -hp 12345
其中 -h 表示显示线程,-p 指定进程。假设列表中线程 id 12567 持续占用较高 cpu,需要把它转换为十六进制,因为 jstack 中线程的 nid 通常以十六进制显示。
printf '%x\n' 12567
输出假设为:
3117
接下来抓取线程快照:
jstack -l 12345 > /tmp/jstack-12345-1.log
在快照中搜索对应的 nid:
rg -n -i 'nid=0x3117' /tmp/jstack-12345-1.log
如果服务器没有 rg,也可以使用:
grep -n -i 'nid=0x3117' /tmp/jstack-12345-1.log
找到线程后,重点查看它的名称、状态和调用栈。例如:
"order-calculate-17" #156 prio=5 tid=... nid=0x3117 runnable
java.lang.thread.state: runnable
at com.example.order.priceservice.calculate(priceservice.java:87)
at com.example.order.orderservice.createorder(orderservice.java:126)这说明热点线程正在 priceservice.calculate() 附近运行,但一次快照还不能直接证明问题就在第 87 行。线程可能只是恰好执行到这里。
更可靠的做法是间隔数秒抓取 3~5 次快照:
jstack -l 12345 > /tmp/jstack-1.log # 间隔几秒后再次执行 jstack -l 12345 > /tmp/jstack-2.log jstack -l 12345 > /tmp/jstack-3.log
如果同一个高 cpu 线程连续停留在相同或相邻调用栈,才更能说明该方法存在死循环、重复计算或热点逻辑。
三、常见根因应该怎么看?
1. 死循环或循环条件错误
最典型的情况是线程长期处于 runnable,多次快照都落在同一段代码。
while (index < tasks.size()) {
task task = tasks.get(index);
process(task);
// 忘记 index++,线程会持续空转
}除了显式死循环,还要检查失败后立即重试、消息反复回队、分页游标没有推进等“业务死循环”。这类问题表面上没有 while (true),却同样会持续消耗 cpu。
2. 频繁 gc
当对象创建速度过快、堆空间过小或存在内存泄漏时,gc 线程可能占用大量 cpu。此时不要只看业务线程栈,还要观察 gc 指标。
jstat -gcutil 12345 1000 10
重点关注 ygc、fgc 的增长速度以及老年代占用。如果 full gc 频繁发生,通常需要继续分析 gc 日志和堆内对象,而不是把结论简单归为“gc 参数不合理”。常见根因还包括一次加载过多数据、大量临时对象、缓存无限增长和不合理的序列化。
3. 线程池空转或任务堆积
轮询任务如果没有阻塞等待,在没有任务时仍然不断查询,也会造成 cpu 空转。
while (!thread.currentthread().isinterrupted()) {
runnable task = queue.poll();
if (task != null) {
task.run();
}
}更合理的方式是使用 take()、带超时的 poll() 或由事件唤醒。同时检查线程池活跃线程数、队列长度、拒绝次数和任务耗时:cpu 高有时不是某一个任务异常,而是上游瞬间提交了过多计算任务。
4. 锁竞争一定会让 cpu 飙高吗?
不一定。大量线程处于 blocked、waiting 时,常见表现是吞吐下降、load average 升高,但 cpu 未必很高。不过,自旋锁、cas 高频失败或极短临界区的激烈竞争可能消耗大量 cpu。
因此看到锁栈时,还要结合线程状态和采样结果判断,不能仅凭“代码中有锁”就下结论。
5. 序列化、正则表达式和大集合计算
以下问题也经常成为热点:
- 对大对象反复进行 json 序列化;
- 灾难性回溯的正则表达式;
- 大集合频繁排序、去重或复制;
- 加密、压缩和图片处理;
- 日志中拼接巨大字符串;
- sql 返回海量数据后在 jvm 内进行聚合。
这类问题在线程栈里往往只能看到某个库方法,最好进一步通过 cpu 采样确认各方法的实际占比。
四、使用 arthas 快速定位
如果生产环境允许接入 arthas,可以减少线程 id 转换和手工匹配的工作。
查看整体状态:
dashboard
列出当前最繁忙的 5 个线程:
thread -n 5
查看指定线程:
thread <thread-id>
当线程栈不足以定位热点时,可以进行一段时间的 cpu 采样:
profiler start --event cpu profiler status profiler stop --format html
火焰图中横向宽度表示该调用路径采样出现的频率。越宽的方法通常消耗越多 cpu。应从宽热点向上追踪业务入口,结合流量和调用次数判断:一个方法总耗时高,可能是单次执行慢,也可能只是调用次数特别多。
注意,线上诊断工具同样会产生开销。采样时间应可控,避免在高峰期长时间执行高开销的追踪命令;trace、watch 等命令也应设置匹配范围和次数。
五、线上故障中的止损顺序
定位根因需要时间,但线上影响可能仍在扩大。通常可以按以下优先级止损:
- 保存现场:记录告警时间、pid、流量、发布记录,抓取线程栈和必要的 gc 信息;
- 隔离异常实例:从负载均衡中摘除问题实例,防止继续接收流量;
- 限流或关闭问题入口:如果热点来自单个接口或任务,优先缩小影响范围;
- 必要时滚动重启或回滚:不要同时重启全部实例,避免容量瞬间归零;
- 修复后补充监控:为线程池、gc、接口耗时、队列积压和容器限流建立告警。
如果 cpu 已经打满,诊断命令可能难以及时执行。此时应先摘流或限流,为诊断留出资源,而不是在故障实例上连续运行多个重型工具。
六、容易踩的排查误区
误区 1:cpu 高就先重启
重启可以恢复服务,却会丢失线程现场。除非业务影响要求立即止损,否则至少先抓取线程栈、gc 状态和监控截图。
误区 2:看到 runnable 就认为线程占用 cpu
runnable 只表示线程在 jvm 看来可以运行,也可能正在等待某些系统调用。必须与操作系统记录的线程 cpu 占用结合判断。
误区 3:只抓一次 jstack
单次快照只是瞬间状态。连续快照或 cpu 采样,才能更可靠地区分偶然经过与持续热点。
误区 4:找到热点方法就立即优化代码
还要结合请求量、数据规模、发布变更和依赖状态。热点方法可能没有逻辑错误,只是突然承受了异常流量。
误区 5:把加机器当成最终方案
扩容可以缓解合理的计算压力,却无法根治死循环、无限重试和任务风暴。资源扩容属于容量手段,不等于根因修复。
七、一份可直接收藏的命令清单
# 1. 查看系统与各 cpu 核心 top mpstat -p all 1 # 2. 找高 cpu 进程 top -c pidstat -u 1 5 # 3. 找进程中的高 cpu 线程 top -hp <pid> # 4. 把十进制线程 id 转成十六进制 printf '%x\n' <thread-id> # 5. 抓取并搜索线程栈 jstack -l <pid> > /tmp/jstack.log grep -n -i 'nid=0x<hex-thread-id>' /tmp/jstack.log # 6. 观察 gc jstat -gcutil <pid> 1000 10
以上就是java应用cpu飙高排查的实战流程的详细内容,更多关于java应用cpu飙高排查的资料请关注代码网其它相关文章!
发表评论