服务器Java进程 CPU 高占用如何定位代码
2026/9/8 16:38:54 网站建设 项目流程

以前写过一篇博客:怎么用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 很低,初步排除其他问题

⚠️ 注意:topCOMMAND列只显示前 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 的格式

jstacknid的显示格式可能是十进制,也可能是十六进制,不同 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.log

3.3 搜索为空的常见原因

如果按nid搜不到,通常有两个原因:

  1. topjstack不是同一时刻抓的,线程可能被回收重建,PID 变了;
  2. nid格式没对上,十进制和十六进制搞混了。

解决办法:重新同时抓top -Hpjstack,或者先用线程名模糊搜索确认当前线程的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"线程名
#96JVM 内部线程编号
[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.sleepwait(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 GCjstat -gcutiljmap -histo分析内存
栈顶是sun.nio.ch.Net.poll/SelectorImplNetty / NIO 事件循环看连接数、消息量是否过大
栈顶是jdk.internal.misc.Unsafe.park线程池线程空闲等待一般不是它导致 CPU 高
栈顶是java.util.concurrent.locks锁竞争检查锁粒度、并发控制

五、辅助排查手段

jstack定位到大致方向后,通常还需要结合其他工具确认根因。

5.1 看 GC 是否正常

如果top -Hp里高 CPU 的是GC Thread#0GC 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

这个命令会打印存活对象数量排行。如果某个业务对象数量异常多,比如SomeEntitybyte[]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

最后整理一个可以照搬的排查流程:

  1. top看整体 CPU、内存、wa,确认是用户态高还是 IO 等待高。
  2. cat /proc/<PID>/cmdline确认是哪个 Jar 包。
  3. top -Hp <PID>找到具体高 CPU 的线程,记录线程 PID。
  4. jstack -l <PID> > /tmp/jstack.log抓线程栈。
  5. jstacknid是十进制还是十六进制:如果是十进制,直接用 PID 搜;如果是十六进制(带0x),再用printf "%x\n"转换。
  6. grep -A 80 "nid=..." /tmp/jstack.log查看目标线程栈,必要时把-A加大到 100+。
  7. 从下往上找业务代码,穿透 MyBatis / Spring 代理 / 反射层。
  8. 判断线程状态RUNNABLE看栈顶在干什么,BLOCKED看锁竞争,WAITING一般不是 CPU 元凶。
  9. 结合辅助工具jstat看 GC、jmap看内存、SHOW PROCESSLIST看 SQL、日志看异常。
  10. 定位到具体代码后,再决定是加索引、改 SQL、拆循环、加缓存、还是调整线程池配置。

七、几个关键点

  1. top线程名被截断到 15 个字符,不要凭截断名搜索,要结合 PID 搜nid(注意 nid 可能是十进制或十六进制)。
  2. topjstack尽量同时抓,调度线程池里的线程可能被回收重建,不同时间抓到的 nid 可能对不上。
  3. grep -A 40可能不够,MyBatis + Spring AOP + JDK 反射会把业务方法包得很深,建议-A 80-A 100
  4. 不要看到waiting on condition就放松警惕,线程可能只是在等下一次调度,但它累计 CPU 时间依然很高。
  5. RUNNABLE不一定在跑业务代码,也可能卡在Net.poll等 MySQL Socket 读,要结合栈顶判断。

这次排查本质上就是:CPU 高 ≠ 业务代码在死循环,也可能是线程频繁发起慢 SQL、等 MySQL 结果。通过top定位进程 →top -Hp定位线程 →jstack定位栈 → 结合数据库/日志确认根因,基本能覆盖大部分 Java 高 CPU 场景。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询