以前写过一篇博客:怎么用idea分析hprof文件定位JVM内存问题 。
然后有小伙伴私信说要怎么定位Jar包CPU高占用的问题。正好最近有个真实场景感觉很不错,记录一下。
我是从定位Java进程开始。如果你能确定你的Java进程和项目,那就跳过第一步。
一、定位Java进程
top是定位服务器资源使用率最好用的工具之一,想了解的可以去搜搜它的使用和具体参数含义。现在我们最主要关注的几个参数我用红框起来了:
记住它的PID。本示例即:2523890
1.1 分析占用
us(用户态)77.7% +sy(内核态)12.3%,几乎无空闲。%CPU107.6%,说明它占用了超过一个物理核。wa接近 0,这里其实就能初步判断:问题在 JVM 应用内部,不是系统 IO 等待。
1.2 定位具体Jar包
如果你的服务器上只跑了一个Jar包,那就不用这步操作了,肯定是它。
但如果不只跑了一个Jar包,那就可以去定位一下到底是哪个Jar包:
# 自行调整PIDcat/proc/2523890/cmdline|tr'\0'' '这样就能定位到具体的Jar包了,我这边是inspection-platform-test.jar这个Jar。
二、找到具体线程
找到Jar包之后,我们要继续定位是这个Jar的什么线程导致的。
2.1 查看进程内线程 CPU 占用
我们可以继续用top去查看这个进程下的所有线程
top-Hp2523890很容易我们就定位到了:
- 三个线程
my-task-schedul分别占31.6%、26.3%和21.1%,合计近 90%。 TIME+累计 CPU 时间达到 100+ 分钟,说明是持续性的,不是偶发 spike。- 其他线程(Redisson、GC Thread)CPU 很低,初步排除其他问题。
⚠️ 注意:
top的COMMAND列只显示前 15 个字符,线程名可能被截断。实际线程名可能是my-task-scheduling-1这种更长的名字。
值得一提的是,这个前缀在我的系统内是定时任务的专用线程池的线程前缀,定位起来就有个大方向了。
因此,也建议没有配置线程池线程名称前缀的小伙伴配置起来,后续在定位问题方面会方便非常多。
三、抓取 Java 线程栈
第一步是定位Jar的进程号,第二步是定位现在我们定位到了是my-task-schedul这个线程池导致的问题。但这里面范围还是很大,因此我们可以继续去定位具体的堆栈。
3.1 抓栈
建议多抓几次,间隔 3~5 秒,便于观察线程状态变化:
jstack-l2523890>/tmp/jstack_1.logsleep3jstack-l2523890>/tmp/jstack_2.logsleep3jstack-l2523890>/tmp/jstack_3.log之后我们就可以根据这个日志来定位堆栈了。
3.2 搜索目标线程的两种方式
找到高 CPU 线程的 PID 后,下一步就是在jstack里找到对应的线程栈。一般有两种搜索方式。
方式一:按线程名模糊搜索(推荐先做这个)
适合先整体看看这个线程池里有哪些线程、分别是什么状态:
grep-i"task-sched"/tmp/jstack_1.log|head-n20输出示例:
这里能直接看到:
- 哪些线程是
runnable(正在干活,重点关注); - 哪些线程是
waiting on condition(空闲或等下一次调度); - 每个线程的
nid就是操作系统线程 ID,和top -Hp里的 PID 是一回事。
方式二:按具体 PID / nid 精确搜索
已经确定某个 PID 高 CPU 后,直接查这个线程的完整栈:
# 注意 nid 后面加个空格,避免 2525536 匹配到 25255360grep-A100"nid=2525536 "/tmp/jstack_1.log-A 100表示把匹配行后面 100 行也打出来。
关于 nid 的格式
jstack里nid的显示格式可能是十进制,也可能是十六进制,不同 JDK 版本表现不一样。
老版本 JDK 常见这样:
"thread-xxx" #12 nid=0x268961 ...而 Java 21 OpenJDK 可能直接显示十进制:
"my-task-scheduling-5" #96 ... nid=2525534 ...所以先看一下你的jstack输出里nid是什么格式:
- 如果是十进制,直接拿
top -Hp里的 PID 搜就行; - 如果是十六进制(带
0x前缀),需要把 PID 转一下:
printf"%x\n"2525537# 268961grep-A100"nid=0x268961"/tmp/jstack_1.log3.3 搜索为空的常见原因
如果按nid搜不到,通常有两个原因:
top和jstack不是同一时刻抓的,线程可能被回收重建,PID 变了;nid格式没对上,十进制和十六进制搞混了。
解决办法:重新同时抓top -Hp和jstack,或者先用线程名模糊搜索确认当前线程的nid。
四、分析线程栈
找到线程之后,最重要的就是看懂jstack里的栈信息,从而判断这个线程到底在干什么。
4.1 jstack 输出格式
一条典型的线程栈长这样:
"my-task-scheduling-5" #96 [2525534] daemon prio=5 os_prio=0 cpu=5351201.84ms elapsed=65557.81s tid=0x00007ff13737e280 nid=2525534 runnable [0x00007ff090cfd000] java.lang.Thread.State: RUNNABLE at sun.nio.ch.Net.poll(java.base@21.0.12/Native Method) at sun.nio.ch.NioSocketImpl.park(...) at java.net.Socket$SocketInputStream.read(...) at com.mysql.cj.jdbc.ClientPreparedStatement.execute(...) at org.apache.ibatis.executor.statement.PreparedStatementHandler.update(...) ...几个关键字段:
| 字段 | 含义 |
|---|---|
"my-task-scheduling-5" | 线程名 |
#96 | JVM 内部线程编号 |
[2525534] | 操作系统线程 ID(TID),和top -Hp里的 PID 对应 |
nid=2525534 | 也是操作系统线程 ID,十六进制写法是nid=0x268961 |
cpu=5351201.84ms | 该线程累计消耗的 CPU 时间 |
java.lang.Thread.State | 线程状态:RUNNABLE / WAITING / TIMED_WAITING / BLOCKED |
at ... | 调用栈,从下往上是调用顺序 |
4.2 线程状态怎么看
| 状态 | 含义 | 是否可能耗 CPU |
|---|---|---|
RUNNABLE | 正在运行,或等待 CPU/IO | 可能高 CPU |
WAITING | 无限等待某个条件/锁 | 一般不耗 CPU |
TIMED_WAITING | 限时等待,比如Thread.sleep、wait(timeout) | 一般不耗 CPU |
BLOCKED | 等待监视器锁,竞争激烈 | 可能伴随高 CPU |
高 CPU 场景下,我们重点看RUNNABLE的线程;BLOCKED多了说明有锁竞争。
4.3 定位到业务代码
比如这次排查中,一个线程的栈顶是这样的:
at sun.nio.ch.Net.poll(...) at java.net.Socket$SocketInputStream.read(...) at com.mysql.cj.jdbc.ClientPreparedStatement.execute(...) at org.apache.ibatis.executor.statement.PreparedStatementHandler.update(...) at java.lang.reflect.Method.invoke(...) at org.apache.ibatis.plugin.Plugin.invoke(...)可以继续把grep -A的行数加大,比如-A 80、-A 100,继续往下翻:
grep-A100"nid=2525534 "/tmp/jstack_new.log继续往下就能看到:
at com.ny.insp.dao.IpDroneArchiveDao.updateOnlineStatus(IpDroneArchiveDao.java:153) at com.dji.sample.manage.service.impl.DeviceOfflineLogScheduledService.updateOnlineStatus(...) at com.dji.sample.manage.service.impl.DeviceOfflineLogScheduledService.checkDevice(...) ... at org.springframework.scheduling.support.ScheduledMethodRunnable.run(...)到这里就清晰了:这是一个 Spring 定时任务,在逐条更新数据库。那后续就是优化一下这部分的业务代码了,这里就不提了。
4.4 常见高 CPU 栈顶场景
| 栈顶特征 | 可能原因 | 处理方向 |
|---|---|---|
栈顶是你自己的业务类,比如比如 com.xxx.service.XxxService.someHeavyMethod | 死循环、大量计算、大数据量遍历 | 直接看代码逻辑 |
栈顶是java.net.SocketInputStream.read/com.mysql.cj... | SQL 慢查询、N+1 查询、大结果集 | 查 MySQL 慢日志、加索引、批量查询 |
栈顶是GC Thread#*/VM Thread | 频繁 GC / Full GC | jstat -gcutil、jmap -histo分析内存 |
栈顶是sun.nio.ch.Net.poll/SelectorImpl | Netty / NIO 事件循环 | 看连接数、消息量是否过大 |
栈顶是jdk.internal.misc.Unsafe.park | 线程池线程空闲等待 | 一般不是它导致 CPU 高 |
栈顶是java.util.concurrent.locks | 锁竞争 | 检查锁粒度、并发控制 |
五、辅助排查手段
jstack定位到大致方向后,通常还需要结合其他工具确认根因。
5.1 看 GC 是否正常
如果top -Hp里高 CPU 的是GC Thread#0、GC Thread#1这类 JVM 线程,优先看 GC:
jstat-gcutil2523890100010输出示例:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 0.00 75.00 95.00 97.50 95.20 1234 12.345 89 56.789 69.134重点关注:
O(老年代)是否接近 100% 且下不来;FGC次数是否快速增长;FGCT(Full GC 总时间)是否持续增加。
如果老年代快满了、FGC 很频繁,基本是内存泄露或堆内存配小了。
5.2 看内存里的对象排行
jmap-histo:live2523890|head-n30这个命令会打印存活对象数量排行。如果某个业务对象数量异常多,比如SomeEntity、byte[]、HashMap$Node暴增,就能快速定位到内存热点。
5.3 看数据库慢查询
如果栈里卡在 MySQL,一定要去数据库侧确认:
-- 看当前正在执行的 SQLSHOWFULLPROCESSLIST;-- 看慢查询汇总(需要 performance_schema)SELECTDIGEST_TEXT,COUNT_STAR,SUM_TIMER_WAIT/1000000000000AStotal_seconds,AVG_TIMER_WAIT/1000000000000ASavg_secondsFROMperformance_schema.events_statements_summary_by_digestORDERBYSUM_TIMER_WAITDESCLIMIT10;很多时候,Java 侧 CPU 高其实是 MySQL 慢查询导致的:线程卡在等 SQL 结果,或者一遍又一遍地执行慢 SQL。
5.4 看业务日志
最后别忘了看应用日志,特别是异常、超时、重复重试相关的日志:
tail-f/path/to/app.loggrep-E"ERROR|Exception|timeout|rejected"/path/to/app.log|tail-n50如果你的日志链路带了 traceId,可以直接用 traceId 把一次请求的完整链路串起来。
背八股还是有点用的好像。。
六、完整排查 Checklist
最后整理一个可以照搬的排查流程:
top看整体 CPU、内存、wa,确认是用户态高还是 IO 等待高。cat /proc/<PID>/cmdline确认是哪个 Jar 包。top -Hp <PID>找到具体高 CPU 的线程,记录线程 PID。jstack -l <PID> > /tmp/jstack.log抓线程栈。- 看
jstack里nid是十进制还是十六进制:如果是十进制,直接用 PID 搜;如果是十六进制(带0x),再用printf "%x\n"转换。 grep -A 80 "nid=..." /tmp/jstack.log查看目标线程栈,必要时把-A加大到 100+。- 从下往上找业务代码,穿透 MyBatis / Spring 代理 / 反射层。
- 判断线程状态:
RUNNABLE看栈顶在干什么,BLOCKED看锁竞争,WAITING一般不是 CPU 元凶。 - 结合辅助工具:
jstat看 GC、jmap看内存、SHOW PROCESSLIST看 SQL、日志看异常。 - 定位到具体代码后,再决定是加索引、改 SQL、拆循环、加缓存、还是调整线程池配置。
七、几个关键点
top线程名被截断到 15 个字符,不要凭截断名搜索,要结合 PID 搜nid(注意 nid 可能是十进制或十六进制)。top和jstack尽量同时抓,调度线程池里的线程可能被回收重建,不同时间抓到的 nid 可能对不上。grep -A 40可能不够,MyBatis + Spring AOP + JDK 反射会把业务方法包得很深,建议-A 80或-A 100。- 不要看到
waiting on condition就放松警惕,线程可能只是在等下一次调度,但它累计 CPU 时间依然很高。 RUNNABLE不一定在跑业务代码,也可能卡在Net.poll等 MySQL Socket 读,要结合栈顶判断。
这次排查本质上就是:CPU 高 ≠ 业务代码在死循环,也可能是线程频繁发起慢 SQL、等 MySQL 结果。通过top定位进程 →top -Hp定位线程 →jstack定位栈 → 结合数据库/日志确认根因,基本能覆盖大部分 Java 高 CPU 场景。