当前位置: 代码网 > it编程>编程语言>Java > Java应用CPU飙高排查的实战流程

Java应用CPU飙高排查的实战流程

2026年08月09日 Java 我要评论
线上告警突然提示 cpu 使用率达到 90%,接口延迟也开始上升。此时你会先重启服务,还是立刻执行 jstack?重启也许能暂时止损,但会破坏最有价值的故障现场;盲目抓线程栈,又可能面对几百个线程无从

线上告警突然提示 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 进程繁忙?

一台机器上通常不止一个进程。先通过 toppidstat 找到真正消耗 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 飙高吗?

不一定。大量线程处于 blockedwaiting 时,常见表现是吞吐下降、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。应从宽热点向上追踪业务入口,结合流量和调用次数判断:一个方法总耗时高,可能是单次执行慢,也可能只是调用次数特别多。

注意,线上诊断工具同样会产生开销。采样时间应可控,避免在高峰期长时间执行高开销的追踪命令;tracewatch 等命令也应设置匹配范围和次数。

五、线上故障中的止损顺序

定位根因需要时间,但线上影响可能仍在扩大。通常可以按以下优先级止损:

  1. 保存现场:记录告警时间、pid、流量、发布记录,抓取线程栈和必要的 gc 信息;
  2. 隔离异常实例:从负载均衡中摘除问题实例,防止继续接收流量;
  3. 限流或关闭问题入口:如果热点来自单个接口或任务,优先缩小影响范围;
  4. 必要时滚动重启或回滚:不要同时重启全部实例,避免容量瞬间归零;
  5. 修复后补充监控:为线程池、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飙高排查的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com