Java线上CPU 100%排查指南:先取证再修复,从top到jstack实战

发布时间:2026/9/10 17:43:46
Java线上CPU 100%排查指南:先取证再修复,从top到jstack实战 线上 CPU 直接打满 100%运维电话一个接一个群里一遍又一遍这场景做 Java 后端的人多少都经历过。每次处理这种故障我最担心的反而不是 CPU 本身多严重而是处理的人第一反应是什么。坦诚说这些年我接手过不少线上事故复盘见到最多的动作就是——二话不说先把服务重启了。重启一时爽现场全烧光。CPU 是降下来了但为什么打满、下次什么时候会再打满全成了谜。这篇文章就围绕Java 线上 CPU 100% 怎么查这件事把我自己的完整排查思路、命令、分析技巧和踩过的坑一次说清楚。适合刚接触线上问题排查的同学从头看到尾也适合有几年经验的开发拿来自查复盘。核心就一句话第一步别急着处理先取证。1. 线上 CPU 100%先别慌搞懂根因比动手更重要1.1 CPU 100% 的两种完全不同的成因先说一个绝大多数人忽略的基础判断CPU 100% 只是结果成因分两大类处理方法完全不同。第一类是业务流量或计算量真实上涨。比如大促、秒杀、定时任务集中跑批流量翻了几倍应用确实需要这么多 CPU。这种情况往往伴随请求量指标同步上涨QPS、P99 延时、活跃线程数都在爬坡。它的本质是不够用解法是扩容、限流、削峰填谷而不是去改代码。第二类是应用内部出现异常CPU 被迫空转或疯狂干活但业务量并没有明显变化。流量曲线是平的CPU 却突然从 20% 飙到 100%。这才是程序自身的问题可能是某个线程进了死循环、正则表达式灾难性回溯、频繁 Full GC 导致垃圾回收线程把 CPU 吃满也可能是并发容器出现热锁竞争。线上真正值得紧急处理、也最容易在第一步翻车的是第二种。为什么容易翻车因为绝大多数人一看到 CPU 100%第一反应就是重启。重启后 CPU 确实马上下来了监控也恢复了大家松一口气可问题大概率在几小时或者第二天原样重现。这个道理说得直白点CPU 打满相当于程序在疯狂喊救命你把电源一拔它确实安静了但它为什么喊救命你永远不知道。线上排查最重要的是现场证据而重启就是销毁证据最彻底的手段。1.2 为什么先把服务重启一下是下下策我自己刚带团队那会儿也犯过这个错。有一次凌晨报警 CPU 100%我第一反应是先把服务重启想着恢复为先。结果三天内同一个服务在完全不同的节点又爆了三次每次都是重启解决最后业务方忍无可忍我们才硬着头皮在故障期间去抓现场。那次抓完现场才发现是定时任务里一个 while 循环条件写错一旦跑批数据量达到某个阈值就死循环问题在代码里躺了两个月。重启为什么是下下策有几个非常现实的原因线程 Dump 没了CPU 打满时jstack 抓下来的线程堆栈是最直接的证据能直接告诉我们肇事线程正在执行什么代码。重启后这条线索彻底断了。堆内存现场没了如果是 GC 问题jmap 导出的堆快照能看出对象分配异常重启后一切归零。GC 日志如果没持久化也跟着没了很多应用 GC 日志默认没开没有历史数据问题无法对比。问题复现成本高线上流量、数据分布、并发时机都是特定条件重启后碰巧不触发不代表修好了。所以我的原则很简单只要服务还能扛得住别急着重启如果服务已经不可用必须摘流也先把线程 Dump 和堆 Dump 拿到手再优雅停机。摘流优先于重启用负载均衡把节点摘下来让它以一个脱离流量的状态继续运行这时候再从容抓现场。提示如果 CPU 100% 导致服务完全卡死、无法响应健康检查也别直接 kill -9。先执行 jstack 和 jmap 命令这两个命令在进程半死状态下往往还能运行抓到文件后再处理进程。2. 正确的查问题顺序top → thread → stack很多教程上来就让你 jstack但我实测下来不做前两步jstack 的红利你吃不满。一个 Java 进程里有几百上千个线程jstack 会输出一份巨大的文本没有定位目标线程就直接看效率极低、容易看花眼。标准流程应该是一层层缩小范围。2.1 第一步top 锁定肇事进程先确认 CPU 到底是哪个进程吃掉的不要默认就是 Java 的事。登录到服务器后第一件事执行top -ctop 默认按 CPU 使用率排序几秒后就能看到最上面的进程。关键要看%CPU列和COMMAND列。如果机器上有多个 Java 进程-c参数会把完整的启动命令显示出来方便区分是哪个应用。如果 Java 进程 CPU 确实在 90% 以上记下它的 PID进入下一步。这里补充一个容易踩的细节top 里的%CPU默认是单核百分比。比如 8 核机器上一个进程最多显示 800%如果看到一个进程 200%、300%说明它用满了两个或三个核同样属于CPU 异常。你还需要看整体%Cpu(s)的统计确认是不是还有其他进程也在抢资源。2.2 第二步top -Hp 锁定肇事线程拿到进程 PID 后执行top -Hp pid-H参数会切换到线程视图展示这个 Java 进程内部每个线程的 CPU 占用情况。这一步非常关键因为 CPU 100% 往往不是整个进程所有线程都在忙而是某一个或某几个线程在疯狂消耗 CPU其他线程都在正常等待。在线程视图里找到%CPU最高的那个线程记下它的线程 IDPID 列。注意这里的 ID 是操作系统的线程 IDLWP不是 Java 线程池里那个线程编号。后面 jstack 分析时要靠这个 ID 去对应。2.3 第三步线程 ID 转换与 jstack 抓取jstack 输出的线程信息里nid 字段是十六进制形式而 top 里看到的是十进制所以需要一个转换步骤printf %x\n 线程ID比如线程 ID 是 28473执行后得到 6f39这就是 jstack 文件里 nid0x6f39 的线程。然后立刻抓线程快照jstack pid jstack_$(date %Y%m%d_%H%M%S).log建议连续抓 2-3 次每次间隔 3-5 秒。为什么要连续抓因为单次快照只能说明这一刻线程在做什么如果线程刚好在切换可能抓到的是无关紧要的位置。连续抓几次对比同一个 nid 的线程每次都在同一个栈位置基本可以确定它是卡死或死循环在那里。抓完之后在 dump 文件里定位目标线程grep -A 30 nid0x6f39 jstack_xxx.log-A 30表示显示匹配行之后 30 行也就是线程的完整堆栈。到这里问题基本已经缩小到了某个线程的某段代码范围接下来就是分析堆栈内容。3. 线程 Dump 分析三种最常见的 CPU 打满模式线程 Dump 拿到手分析思路其实是有章法的。我见过的大多数 CPU 100% 故障最后都能归到以下三种模式之一。每种模式在 jstack 里的表现不一样排查方向也完全不同。3.1 模式一业务代码死循环或超长计算这是最直白的一种某个业务线程状态是RUNNABLE堆栈反复停留在同一个业务方法上而且多次抓取快照它的堆栈位置几乎不变。典型表现Thread-15 #25 daemon prio5 os_prio0 tid0x00007f1c142c6000 nid0x6f39 runnable [0x00007f1c0d7f4000] java.lang.Thread.State: RUNNABLE at com.example.order.OrderService.calcPrice(OrderService.java:89) at com.example.order.OrderService.processOrder(OrderService.java:45) at com.example.order.OrderJob.run(OrderJob.java:32)看到RUNNABLE且停留在业务代码里第一反应就是去看那段代码有没有 while/for 循环条件错误、有没有递归调用没有出口、有没有超大数据集在循环里做复杂计算。我遇到过一个很典型的案例一个对账任务在循环里对每个订单做正则匹配订单量小的时候没什么感觉某天数据涨到百万级正则回溯复杂度爆炸CPU 直接打满。单纯看代码谁都想不到这么简单的循环会出问题。这类问题还有个排查技巧如果线程名是业务可读的比如order-job-1、schedule-pool-3直接去搜对应线程池的代码如果是 Tomcat 的http-nio-8080-exec-xx则说明是请求线程可以去搜索对应服务的调用链日志。3.2 模式二GC 风暴第二种高频原因线程 Dump 里通常看不到业务线程在忙反而能看到大量GC task thread或者VM Thread占满 CPU。GC 风暴的本质是 JVM 内存管理出了状况堆内存频繁被占满垃圾回收器一遍又一遍地全量回收回收完马上又满了如此往复。这种模式在 jstack 里的典型特征是VM Thread os_prio0 tid0x00007f1c1419d800 nid0x2d runnable java.lang.Thread.State: RUNNABLE这里的VM Thread在 runnable 状态加上jstat观察到 FGCFull GC 次数快速增加基本可以断定是 GC 问题。要注意这种场景下光看线程 Dump 还不够真正要下结论必须配合 GC 日志和堆 Dump。排查命令# 查看 GC 概况1 秒一次连看 10 次 jstat -gcutil pid 1000 10 # 查看 GC 明细 jstat -gc pid 1000 10jstat -gcutil输出里重点关注 FGC 列Full GC 次数和 FGCT 列Full GC 耗时。如果 FGC 在短短几秒内从 10 跳到 30每次 FGCT 都是几百毫秒GC 风暴实锤。GC 风暴的根因也有很多变种堆大小设置不合理、代码里某个集合无脑 add 导致大对象堆积、缓存框架没有淘汰策略、甚至是一次性加载了超大表。要彻底定位需要一个堆 Dumpjmap -dump:live,formatb,fileheap_$(date %Y%m%d_%H%M%S).hprof pid然后用 MAT 或者 VisualVM 分析看哪个对象占了大量内存。注意-dump:live会触发一次 Full GC属于侵入式操作生产环境操作前最好确认好影响但如果已经 GC 风暴了多一次 Full GC 影响也有限。3.3 模式三并发容器与锁竞争导致的自旋第三种模式相对隐蔽堆栈看起来是WAITING或BLOCKED但 CPU 依然居高不下。常见原因包括JDK 内部并发容器在扩容或竞争时进行自旋重试、ConcurrentHashMap在极端情况下的扩容竞争、ThreadPoolExecutor的addWorker自旋等。有个很经典的案例是 Java 7 及更早版本的HashMap在并发 put 时形成环形链表读取时陷入死循环CPU 打满。很多老项目升级到 Java 8 之前都踩过这个坑。Java 8 改用红黑树后这个问题缓解了但并发场景下HashMap仍然存在数据覆盖、死循环风险线上并发环境务必使用ConcurrentHashMap或Collections.synchronizedMap。排查这类问题时堆栈可能在 JDK 类库代码里反复出现同样位置要注意看堆栈上层是哪个业务方法触发。另外锁竞争导致的 CPU 偏高往往还能从 jstack 里看到大量线程处于BLOCKED状态彼此在等同一把锁情况严重的还会形成死锁jstack 文件末尾会有专门的 Found one Java-level deadlock 提示。4. 实操案例一次典型的 CPU 100% 排查全记录光讲理论大家可能还是觉得虚我拿一个真实处理过的案例把完整排查过程展开你们可以看到每一步我到底在做什么、为什么要这么做。4.1 现场情况与初步判断那次是一个订单系统的定时任务服务每天凌晨 2 点跑批某天凌晨 2 点 15 分收到监控报警CPU 已达 700%8 核机器。当时流量曲线完全平稳没有大促没有活动初步判断属于上文的第二类——程序内部问题。我登录服务器后第一步没有去重启而是先执行了top -c确认确实是我们这个 Java 进程占的 CPU。然后执行top -Hp pid看到两个线程的 CPU 都在 300% 以上说明这两个线程各自吃满了三四个核。记下线程 ID 后我立刻连续抓了三份 jstack。这里有个经验夜间跑批任务出问题抓线程 Dump 的窗口通常很充裕因为业务影响有限完全可以从容取证。但如果是白天核心链路 CPU 100%我的建议是先通过网关或负载均衡把故障节点摘掉流量让剩余节点先扛住再从容抓现场而不是让整个服务处于瘫痪状态干等。4.2 逐步定位到具体代码拿到 jstack 后我先把两份 dump 里 CPU 最高的两个线程 ID 分别转成十六进制用grep -A 40找到对应的线程堆栈。两次抓取的结果非常一致线程状态都是RUNNABLE堆栈停在同一个类DataReconciler.java的某个方法约 40 行附近。我当时的第一反应是死循环但看了代码后并没有发现明显的循环条件问题。再细看发现方法里调用了excelReportService.export()导出报表时要遍历一个ListMapString, Object内部还嵌套了一个字符串拼接的循环。问题就出在这个导出逻辑数据量从平时的两万行暴涨到五十万行后嵌套 for 循环必须遍历所有组合复杂度是 O(n²)五万行还可以接受五十万行瞬间变成天文数字。更糟的是循环里还做字符串拼接每次拼接都会产生新的字符串对象进一步加重 GC 负担。两个因素叠加CPU 直接打满。4.3 根因与修复方案这个案例的根因总结下来是批任务从能跑到跑不动的阈值被数据量触发了——O(n²) 的循环 循环内字符串拼接。修复方案也很有意思不是加机器而是做三件事把嵌套循环改成一次遍历 Map 分组将复杂度从 O(n²) 降到 O(n)循环内的字符串拼接改为StringBuilder导出逻辑增加数据量上限保护超过阈值直接拒绝并告警避免再次拖垮服务。修复上线后再没复发。这个案例的启示是排查 CPU 100% 不能只盯着谁占 CPU还要顺着堆栈找到业务方法从算法复杂度和数据处理量两个维度一起看否则就算这次定位到方法也不一定找到真正的触发条件。5. 排查工具与辅助手段真实故障现场往往比教科书情况更复杂。基础三板斧 top、jstack、jstat 能解决 80% 的问题但有些场景需要更强的工具。5.1 jstack 的替代与增强ArthasArthas 是阿里开源的 Java 诊断工具线上排查利器。它的好处是不需要提前在应用里埋点直接 attach 到运行中的 JVM 进程对生产环境非常友好。用法也简单# 下载启动器 curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar启动后交互式界面里查看 CPU 占用最高的几个线程thread -n 3这个命令会把 CPU 最高的线程排行和堆栈一次性打出来省去了手动转十六进制再 grep 的流程非常直观。还可以用thread --state RUNNABLE筛选出所有运行状态线程快速判断是否存在一堆线程同时 RUNNABLE 的异常。对于 GC 相关排查Arthas 里也能实时看到堆内存、GC 次数、线程数等整体信息执行dashboard就能看到。我自己的习惯是日常巡检和排查用 Arthas 比较多因为它交互体验好但如果是事故取证、需要把 jstack 文件留存归档我还是会用标准jstack命令毕竟原生命令产出的文件更通用也好放到事故报告里。5.2 jstat 与 jmap 的配合使用排查 CPU 100% 时线程 Dump 看的是谁在跑GC 数据看的是内存是否在作妖两者缺一不可。合理套路是# 先看 GC 整体状态 jstat -gcutil pid 1000 5 # 如果 FGC 持续增长 jmap -dump:live,formatb,fileheap.hprof pidGC 日志最好提前开启别等到排查的时候才发现没有历史数据。建议 JVM 启动参数里固定加上 GC 日志配置比如 Java 8 的写法-Xloggc:/data/logs/gc.log -XX:PrintGCDetails -XX:PrintGCDateStampsJava 11 的写法不太一样用-Xlog:gc*:file/data/logs/gc.log这种新格式。开启目标不是为排查那一次故障而是为历史对比——CPU 100% 往往不是瞬间的之前几小时 GC 频率有没有缓慢上升GC 日志能告诉你完整的演变过程。6. 常见问题与排查技巧实录最后这部分我把这些年实际踩过的坑、总结的习惯列一下算是给这篇文章收个尾。6.1 线程快照抓取失败的几个原因jstack 提示 Unable to open socket file大概率是当前用户和启动 Java 进程的用户不一致比如进程是 deploy 用户起的你用 root 去执行 jstack 会失败需要切换到同一用户。jstack 长时间卡住不动进程处于极端状态线程数巨大或者堆内存严重不足时jstack 本身也可能受影响。这时候多次重试或者加-F参数强制抓取jstack -F pid但-F模式抓出来的信息可能不够完整。容器环境 PID 混淆现在服务基本都在 Docker 里宿主机上用top看到的 Java 进程 PID 是容器外的直接jstack可能抓不到。推荐的做法是docker exec -it container jstack pid直接在容器内部执行。线程 ID 对不上别拿 Java 线程池编号如 Thread-15去做十六进制换算要用操作系统线程 IDnid也就是 top -Hp 输出的 PID 列。6.2 排查抢时间的关键技巧线上故障的黄金处理窗口很短我的经验是把标准动作提前固化下来而不是故障时临时想。强烈建议在每一台生产服务器上提前准备一个collect.sh脚本内容大致是抓三次 jstack、抓一次 heap dump、记录 top 快照、记录 jstat 数据全部按时间戳命名归档。故障发生时只需执行一个脚本证据就到手了。另外CPU 100% 的现场有个特点你现在不抓五分钟后再抓可能就好了。很多并发问题触发条件很随机故障是间歇性的。所以一旦报警第一优先永远是取证而不是修复。哪怕只是拿到一份 jstack后面分析也才有据可依。6.3 预防 CPU 100% 的几个习惯排查再熟练也不如不发生。几个我在团队里强制推行的习惯定时任务、批处理类代码必须压测到目标数据量的 3 倍以上重点看算法复杂度和内存占用循环内禁止做字符串拼接、禁止写复杂正则、禁止不设上限的集合扩容JVM 参数提前配好 GC 日志、-XX:HeapDumpOnOutOfMemoryError、-XX:HeapDumpPath确保出问题时有现场线上监控不能只看 CPU要配套线程数、GC 次数、Full GC 耗时、P99 延时等多维指标CPU 100% 之前往往已经有很多小信号。根据我个人的经验线上 CPU 100% 这类问题70% 的情况下靠top 定位线程 jstack 分析堆栈 jstat 验证 GC这套组合拳就能解决剩下 30% 需要堆 Dump 和更深层的分析。但无论哪种方向第一步永远都是把现场证据抓全。别急着重启别急着 kill让子弹飞一会儿——用这几分钟把该拿的文件拿到手后续一切排查都顺畅得多。