一、故障现象
上周五下午,运维同事突然在群里@我,说生产环境一台 Java 应用服务器的 CPU 直接飙到 100%,而且持续了十几分钟都没降下来。当时第一反应是「又出事了」,赶紧打开监控面板看了一眼,CPU 曲线几乎是垂直拉满,接口响应时间也从几十毫秒涨到了好几秒。
这种问题在 Java 项目里其实不算罕见,但每次遇到都得花不少时间定位。今天把这次完整的排查过程记录下来,从发现故障到最终定位到问题代码,希望能给同样踩坑的朋友一些参考。
二、初步排查:先看进程和线程
接到告警后,我第一时间登录服务器,先用top命令确认到底是哪个进程在吃 CPU:
top -c -p $(pgrep -f 'java.*app.jar' | head -1)输出里能看到某个 Java 进程的 CPU 占用率接近 100%,基本可以确定问题就出在这个进程内部。接下来需要进一步定位是哪个线程在疯狂占用 CPU,这时候可以用top -H查看线程级别的 CPU 占用:
top -H -p 12345记下 CPU 占用最高的那个线程的 PID(假设是 23456),后面 dump 分析要用到。
三、生成线程 dump 并分析
确认了问题线程后,我执行了jstack命令抓取线程快照。这里有个小技巧:建议连续抓 3 次,间隔 3 到 5 秒,因为单次快照可能刚好抓到线程切换的瞬间,多次抓取能更准确地判断线程是否一直卡在同一个地方。
jstack 12345 > /tmp/thread_dump_1.txt sleep 3 jstack 12345 > /tmp/thread_dump_2.txt sleep 3 jstack 12345 > /tmp/thread_dump_3.txt抓完 dump 之后,把之前记下的线程 PID(23456)转成十六进制,因为 jstack 输出里的线程 ID 是十六进制的:
printf '%x\n' 23456假设输出是5ba0,然后在 dump 文件里搜索这个值:
grep -A 20 '5ba0' /tmp/thread_dump_1.txt四、定位问题代码
搜索结果很快锁定了问题线程的调用栈,发现它一直卡在一个while循环里,而且循环内部还在做字符串拼接操作。顺着调用栈找到对应的业务代码,问题就清楚了:
public String processData(List<String> items) { String result = ""; int index = 0; while (index < items.size()) { // 这里有个逻辑错误:index 没有递增,导致死循环 result += items.get(index); // 应该加上 index++,但代码里漏掉了 } return result; }问题很明显:while循环里index没有递增,导致循环条件永远成立,线程就在这个死循环里空转,CPU 自然被吃满。而且循环里用+=做字符串拼接,每次都会创建新的 String 对象,进一步加剧了 CPU 和内存的开销。
五、修复与验证
修复方式很简单,补上index++,顺便把字符串拼接改成StringBuilder,避免频繁创建对象:
public String processData(List<String> items) { StringBuilder result = new StringBuilder(); for (int i = 0; i < items.size(); i++) { result.append(items.get(i)); } return result.toString(); }代码修复后重新打包发布,再观察监控面板,CPU 很快就降到了正常水平,接口响应时间也恢复了。整个排查过程从接到告警到定位问题,大概花了 40 分钟左右。
六、经验总结
这次故障排查有几个点值得记下来:
- 先看线程再 dump:用
top -H找到问题线程,能大幅缩小排查范围,不用在几百个线程里瞎找。 - 多次抓取 dump:单次快照可能误判,连续抓几次对比,确认线程是否一直卡在同一位置。
- 线程 ID 要转十六进制:jstack 输出的是十六进制线程 ID,直接搜十进制会找不到。
- 死循环往往伴随对象创建:循环里做字符串拼接这类操作,会让 CPU 和内存问题同时放大。
最后想说,线上问题排查最重要的是冷静和有条理。按照「进程 → 线程 → 调用栈 → 代码」这条链路一步步来,大多数 CPU 飙升的问题都能在半小时内定位。希望这篇记录能帮到正在排查类似问题的你。