Java 线上 CPU 100%,大部分人第一步就走错了方向

张开发
2026/4/21 23:20:45 15 分钟阅读

分享文章

Java 线上 CPU 100%,大部分人第一步就走错了方向
Java 进程 CPU 100%打开终端敲的第一条命令决定了接下来是十分钟定位还是折腾半夜。大部分人第一反应是top。能看到进程级别的 CPU 占用但 Java 一个进程跑着上百个线程光知道 PID 12345 吃满了 CPU 等于什么都没说。第一条命令应该是带-H的top -Hp 12345-H让 top 展示线程级别的资源占用。排在最上面的线程就是吃 CPU 的那个记住它的 LWP线程 ID比如 12378。下一步十进制转十六进制printf %x\n 12378 # 输出 305a然后 jstack 抓线程快照用这个十六进制值搜jstack 12345 | grep -A 30 nid0x305a到这里能拿到吃 CPU 的那个线程的完整堆栈。教程通常写到这里就结束了——”看堆栈定位代码”。但实际排查中堆栈只是线索线程状态才是方向标。看 State不是看堆栈jstack 输出的每个线程都有一行java.lang.Thread.State后面跟着状态。不同的状态指向完全不同的问题。RUNNABLE线程正在 CPU 上执行。堆栈停在业务代码里的某个方法上大概率是死循环——while 条件写错了、递归没出口、或者某个 Stream 操作被无限展开。但有一种更隐蔽的情况堆栈停在java.util.regex.Pattern相关的方法帧上。正则回溯能把 CPU 吃得干干净净。(a)b这种嵌套量词碰上输入aaaaaaaaaaaaaac外层a和内层a会反复尝试不同的拆分方式来匹配最后那个c回溯次数随输入长度指数增长。20 个a就能让一个线程跑满好几秒。线上碰到的通常不是(a)b这么明显而是藏在邮箱校验、URL 解析、日志格式匹配这些正则里。排查时 jstack 堆栈会停在java.util.regex.Pattern$GroupHead.match或者Pattern$Curly.match0这类内部方法上连抓三次都在同一位置。短期加超时保护——用独立线程跑匹配超时就中断长期把正则换掉或者用(?a)这种原子组禁止回溯。如果 RUNNABLE 的线程名带 “GC” 字样比如GC task thread#0 (ParallelGC)问题不在业务代码在垃圾回收。这时候转 jstatjstat -gcutil 12345 1000每秒输出一行格式大概长这样S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 97.14 63.47 99.81 94.56 91.03 842 12.054 47 38.234 50.288 0.00 97.14 78.91 99.83 94.56 91.03 842 12.054 48 39.117 51.171 0.00 97.14 14.26 99.79 94.56 91.03 843 12.098 49 40.003 52.101关键看三个东西。O列Old 区使用率稳定在 99% 以上FGC列Full GC 累计次数每隔几秒就涨一次FGCT列Full GC 累计耗时每次涨接近一秒。这就是 GC 风暴——Old 区满了触发 Full GC回收完发现只释放了零点几个百分点几秒后又满再 Full GC无限循环。GC 线程把 CPU 吃满业务线程几乎拿不到执行机会。正常的 GC 长什么样O列会波动——涨到 70-80% 触发 GC回落到 30-40%FGC几小时甚至几天才涨一次。如果 GC 完O列还是 99%说明 Old 区里的对象几乎全是活的要么有泄漏要么堆给小了。GC 风暴的排查不靠 jstack靠堆 dumpjmap -dump:formatb,fileheap.hprof 12345生产环境提前加好-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/tmp/OOM 时自动 dump不用等出事再手动操作。hprof 文件用 MATEclipse Memory Analyzer打开第一步看 Leak Suspects 报告MAT 会自动标出可疑的大对象。如果指向不明确打开 Dominator Tree 按 Retained Heap 排序。这里有个细节Shallow Heap 是对象自身大小Retained Heap 是这个对象被 GC 后能释放的总量。一个 HashMap 自身可能就几十字节但它引用的几百万个 Entry 加起来占了几个 G——排查泄漏看 Retained不看 Shallow。常见的泄漏模式某个 static 的 ConcurrentHashMap 只 put 不 remove数据库查询没加 limit 把整张表捞进了 ArrayListThreadLocal 忘了 remove线程池复用线程导致 value 不断堆积。BLOCKED线程在等锁。jstack 里 BLOCKED 的线程长这样关键信息是waiting to lock 0x00000000c1a23f10这个地址是它在等的锁对象。全文搜这个地址能找到谁持有它exec-7 持有这把锁卡在socketRead0——在等数据库返回。一条慢 SQL 拖住了 exec-7后面 exec-23 和其他几十个线程全排着队等。CPU 高有两层原因一是持有锁的线程本身在做重活慢查询、远程调用它是 RUNNABLE 的独占着 CPU二是 HotSpot 在 park 之前会做自适应自旋几十个线程同时自旋等锁自旋本身也烧 CPU。优化方向不是减少线程数而是让持有锁的线程尽快释放——优化那条慢 SQL或者缩小 synchronized 的范围或者把锁内的 IO 操作挪到锁外面。WAITING / TIMED_WAITING线程在等某个条件。线程池里空闲的工作线程基本都是这个状态正常。但如果大量业务线程卡在 WAITING堆栈通常指向Future.get()、CountDownLatch.await()、BlockingQueue.take()这些地方——被等的那一方出了问题。找到是哪个 Future 没返回、哪个 CountDownLatch 没倒完问题在那边。还有一种情况值得注意线程总数本身不正常。jstack 输出拉到最底下JNI global references上面那行会显示线程总数。正常的 Spring Boot 应用几十到一两百个线程如果看到两三千甚至上万说明有地方在无限制地创建线程——每个请求 new Thread、线程池 maximumPoolSize 没设上限、或者某个定时任务反复创建线程池没关闭。每个线程默认占 1MB 栈内存两千个线程光栈就吃 2GB加上调度开销系统会变得极慢。不是所有 CPU 100% 都该看 jstacktop第一行有几个百分比%us用户态、%sy内核态、%waIO 等待。先看这三个比例再动手两秒钟的事能避免方向跑偏。%us高是 Java 业务代码或 GC 在吃 CPU走上面的 jstack jstat 链路。%sy高通常是系统调用太频繁。最常见的原因就是线程数膨胀几千个线程同时在跑操作系统花在上下文切换上的时间比花在执行业务代码上的还多。vmstat 1看cscontext switch列cs87653每秒接近 9 万次上下文切换sy52%——一半以上的 CPU 花在内核态调度线程上。查一下 Java 进程的线程数ls /proc/12345/task | wc -l超过一千就该排查哪里在创建线程了。%wa高CPU 其实在等磁盘。可能是日志在疯狂刷盘、某个大文件在写、或者 swap 被触发了——物理内存不够换页到磁盘。Java 进程吃 swap 性能会断崖式下跌JVM 的堆页面被换到磁盘上每次 GC 扫描堆都要从磁盘换回来。iostat -x 1看磁盘利用率free -h看 swap 使用量swap used 不为零就要警惕。jstack 还有个前提JVM 得能响应。GC 风暴严重到 STW 几乎不断的时候jstack 可能挂住不返回。这时候kill -3 12345直接给 JVM 发 SIGQUIT 信号JVM 会把线程 dump 打到标准输出通常在应用的 stdout 日志文件里。这个信号走 JVM 内部的 VMThread 处理比 jstack 的 attach 机制更轻量进程很卡的时候成功率更高。进程不会被杀掉SIGQUIT 对 JVM 的作用就是触发 thread dump。如果提前装了 Arthas排查效率还能再上一个台阶# CPU 占用最高的前 5 个线程直接带堆栈 thread -n 5 # 找出持有锁且阻塞其他线程的那个线程 thread -bthread -n 5一条命令顶了top -Hpprintf %xjstack | grep三步。thread -b直接定位持有锁的线程不用手动搜锁地址。Arthas 的dashboard命令更直接一个终端窗口同时展示线程 CPU 占用、内存区域使用率、GC 统计相当于 jstat jstack 的合体——同时看到 Old 区在飙、Full GC 在涨、哪个线程吃 CPU 最多不用来回切命令。不管用 jstack 还是 Arthas线程快照都只是一个瞬间。线程恰好在某个方法上停了一下不代表它一直在那里。间隔三秒连抓三次同一个线程三次都停在同一个位置才能确认for i in 1 2 3; do jstack 12345 /tmp/jstack_$i.log sleep 3 done三份文件 diff 一下稳定出现在同一个堆栈的线程就是目标。线上排查最贵的成本不是找到根因是一开始走错了方向——GC 的问题去翻业务代码锁的问题去加线程IO 等待的问题去优化算法。top看一眼 us/sy/wa 比例jstack看一眼线程状态jstat看一眼 GC 情况三条命令定方向后面再深挖就不会偏太远。

更多文章