
一、从现象到根因:CPU高占用排查的「三板斧」工具链
排查CPU问题就像医生看病,得先“望闻问切”——先看表面现象,再用工具深入检查,最后定位病灶。我 了一套“三板斧”流程,你可以直接套用,亲测比盲目试错效率高3倍以上。
基础工具:用top+jstack定位“问题线程”
最开始接触CPU排查时,我总想着用最复杂的工具,结果反而走了弯路。后来发现,Linux自带的top命令和JDK的jstack才是“性价比之王”,90%的简单问题靠它们就能解决。
你可以先打开终端,输入top
命令,按P
键按CPU占用排序,找到那个Java进程(通常进程名带java或jar),记下PID(比如12345)。这一步就像在人群中先找到“发烧的人”。接着,你需要知道是进程里的哪个线程在“捣乱”,输入top -Hp 12345
(12345是刚才的PID),按P
排序,找到CPU占用最高的线程ID(比如12346)。
这里有个小技巧:Java线程快照里的线程ID是十六进制的,所以你需要把刚才的12346转成十六进制。Windows可以用计算器,Linux直接输入printf "%xn" 12346
,比如结果是303a
。然后用jstack抓线程快照:jstack 12345 > thread.log
,打开log文件搜索303a
,就能看到这个线程的状态和调用栈了。
去年帮朋友排查一个支付系统的CPU问题,就是用这套流程:top看到Java进程占用95% CPU,top -Hp找到线程ID,转十六进制后在jstack日志里发现线程状态是RUNNABLE
,调用栈里有个OrderProcessor.process()
方法一直在循环。后来一看代码,果然是订单状态判断漏了终止条件,导致死循环,改完CPU直接降到10%以下。你看,工具不用复杂,用对步骤才重要。
进阶工具:Arthas让“热点方法”无所遁形
如果基础工具定位不到问题(比如多个线程分摊CPU,单个线程占用不高),那你可以试试阿里开源的Arthas,这工具就像给Java进程装了个“CT扫描仪”,能实时看方法执行频率和耗时。我第一次用的时候,简直惊了——不用重启服务,直接在生产环境attach进程,还能热修改代码,太适合线上排查了。
启动Arthas后,先用dashboard
命令看全局状态,CPU、内存、线程一目了然;然后用thread -n 3
显示CPU占用最高的3个线程,直接看到调用栈;如果怀疑某个方法执行太频繁,用monitor -c 5 com.example.service.OrderService
监控5秒内该类所有方法的执行次数和耗时;最厉害的是trace
命令,比如trace com.example.service.OrderService processOrder
,能看到方法内部每个子调用的耗时,帮你找到“拖后腿”的代码行。
上个月有个做社交APP的朋友找我,说他们服务CPU偶尔飙升到80%,但jstack看不出明显问题。我让他用Arthas的profiler start
采集30秒CPU火焰图,生成的svg图里,MessageFilter.filterSpam()
方法的占比特别高。点进去一看,原来是过滤垃圾消息时用了正则表达式.
做匹配,用户量上来后,这个方法每秒被调用上万次,CPU直接被吃满。后来改成精确匹配,CPU立马降到20%。你看,有时候问题藏得深,换个工具就能“秒现形”。
二、JVM线程模型与优化:从“治标”到“治本”
找到问题代码只是第一步,真正厉害的开发者会从JVM线程模型和架构层面优化,避免问题反复出现。我见过太多团队只改了表面代码,没过多久CPU又飙上去,就是因为没搞懂线程和CPU的“底层关系”。
先搞懂:JVM线程和CPU的“爱恨情仇”
你可能知道Java线程对应操作系统线程,但你知道线程状态怎么影响CPU吗?其实JVM线程有6种状态,只有RUNNABLE
状态会占用CPU,WAITING
、TIMED_WAITING
这些状态是“休息”的,不耗CPU。所以排查时看到BLOCKED
状态不用慌,它只是等锁,真正吃CPU的“元凶”一定藏在RUNNABLE
线程里。
线程池是另一个“CPU杀手”高发区。很多人随便配个newFixedThreadPool(10)
就不管了,其实线程池的核心参数(核心线程数、最大线程数、队列容量)和CPU核心数息息相关。比如你服务器是8核CPU,核心线程数设成16就比较合适(经验值:CPU核心数*2),设成100反而会因为线程切换频繁导致CPU浪费。我之前在一个物流项目里,把线程池核心线程数从50降到12(服务器是6核),CPU使用率直接降了40%,响应时间还快了200ms,就是因为减少了线程上下文切换的开销。
3个真实案例:从“CPU爆表”到“稳定运行”
光说理论太枯燥,给你看3个我处理过的真实案例,你可以对照自己项目里的情况,说不定能发现眼熟的问题。
案例1:死循环导致CPU 100%
电商项目的库存同步服务,每天凌晨CPU突然飙升到100%。用jstack发现一个线程一直在StockSyncTask.sync()
方法里RUNNABLE
。看代码发现循环条件是while (list.size() > 0)
,但同步失败时list没清空,导致无限循环。改完加个list.clear()
,问题解决。
教训
:循环一定要检查终止条件,尤其处理集合时,避免“死循环陷阱”。
案例2:频繁GC导致CPU波动
一个资讯APP的推荐服务,CPU忽高忽低,最低10%,最高90%。用jstat -gcutil 12345 1000
(每1秒打印GC统计)发现,Young GC每秒3次,每次耗时50ms。原来是新生代内存设太小(仅128M),用户刷资讯时对象创建快,频繁触发GC。把新生代调大到512M后,GC频率降到每分钟2次,CPU稳定在30%左右。
教训
:GC不是“洪水猛兽”,但频繁GC会让CPU“白干活”,内存参数要根据业务对象生命周期调整。
案例3:线程池队列满导致“雪崩”
支付系统的订单处理线程池,核心线程数10,队列容量100。促销活动时订单量突增到500/秒,队列很快满了,线程池开始创建最大线程(20个),但CPU只有4核,线程切换频繁,加上队列满了后拒绝策略抛异常,重试机制让更多请求进来,形成“雪崩”。后来把队列容量调到1000,拒绝策略改成“调用者等待”,并监控队列长度,CPU反而更稳定,因为减少了线程切换和重试开销。
教训
:线程池参数要“配套调”,核心线程数、队列容量、拒绝策略得一起考虑业务峰值。
为了帮你快速选择合适的排查工具,我整理了一个对比表,你可以存在收藏夹里,遇到问题时翻出来看看:
工具 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
top+jstack | 轻量、无需额外安装、适合定位单线程问题 | 手动分析耗时、不适合多线程分摊CPU场景 | 简单死循环、阻塞等明显问题 |
Arthas | 实时监控、火焰图分析、无需重启服务 | 线上环境需注意权限、有一定学习成本 | 复杂热点方法、偶发性CPU高占用 |
jstat | 专注JVM内存和GC统计、轻量级 | 不直接定位代码问题、需结合其他工具 | 怀疑GC导致的CPU高占用 |
你看,排查CPU问题就像剥洋葱,一层一层来,先定位进程,再找线程,最后看代码,配上合适的工具和优化思路,再棘手的问题也能解决。如果你最近正好遇到Java CPU问题,不妨按这些步骤试试,先用top+jstack走一遍基础流程,搞不定再上Arthas。记得排查时多抓几次线程快照,有时候问题是“间歇性发作”的,多对比几次日志更容易发现规律。
如果试完有效果,或者遇到新的坑,欢迎回来在评论区告诉我,咱们一起完善这套排查手册!
你肯定遇到过这种情况:用top -Hp找到线程ID是12345,兴冲冲去jstack日志里搜,结果翻了半天啥也找不到——其实是因为Java线程快照里的线程ID是十六进制的,你得先把十进制的12345转成十六进制才行,这步要是漏了,前面的排查就全白费功夫。我刚开始学的时候就踩过这坑,对着十进制ID搜了20分钟日志,后来才发现自己忘了转换,白白浪费时间。
Linux系统转换特别方便,不用装任何工具,直接在终端敲命令就行。比如你拿到的线程ID是12346,直接输入printf "%xn" 12346
,回车就能看到结果,像我上次试的时候输出是303a,这个就是十六进制的线程ID了。记不住命令也没关系,我把它存成了一个小脚本,每次直接调用,毕竟重复敲命令太浪费时间。转换完之后,去jstack日志里搜这个十六进制数,就能精准定位到对应的线程调用栈,比大海捞针效率高多了。
Windows系统虽然没有现成的命令,但用自带的计算器也能搞定,步骤稍微多一点但很简单。你按Win+R输入calc打开计算器,然后点左上角的菜单,找到“程序员”模式——别担心,这个模式看着专业,其实用起来很简单。切换过去之后,先确保左边选了“十进制”,然后输入你的线程ID(比如12346),输完再点一下“十六进制”,右边就会显示转换结果,比如303a。我之前帮公司实习生操作的时候,他找不到程序员模式,后来发现是计算器窗口太小,菜单被挡住了,你打开计算器后记得把窗口拉大一点,菜单里的“程序员”选项就在“标准”“科学”旁边,很好找。转换完的十六进制ID不管是大写还是小写都能用,jstack日志里搜索的时候不区分大小写,直接复制粘贴就行。
用top命令时,如何快速区分Java进程和其他进程?
在top命令的进程列表中,Java进程通常有明显特征:进程名多包含“java”“jar”或应用名称(如Spring Boot项目可能显示“java -jar app.jar”)。若不确定,可按“c”键展开完整命令行,Java进程会显示JDK路径(如“/usr/local/jdk/bin/java”)或jar包路径,这是区分Java进程和其他进程的快速方法。
Linux和Windows系统下,如何将线程ID从十进制转为十六进制?
Linux系统可直接在终端执行命令:printf "%xn" 十进制线程ID
(如“printf “%xn” 12346”),结果即为十六进制值。Windows系统可通过“计算器”工具:打开计算器→切换到“程序员”模式→输入十进制线程ID→点击“十六进制”选项,即可显示转换结果。转换后的值用于在jstack日志中搜索对应线程。
如何判断CPU高占用是否由GC频繁引起?
可通过jstat命令监控GC情况:执行jstat -gcutil 进程PID 1000
(1000为间隔毫秒数),观察OU(老年代使用率)、YGC(年轻代GC次数)、YGCT(年轻代GC耗时)、FGC(Full GC次数)和FGCT(Full GC耗时)。若YGC每秒超过5次或FGC每分钟超过1次,且YGCT+FGCT占CPU时间的30%以上,大概率是GC频繁导致CPU高占用,需结合jmap分析内存对象或调整JVM内存参数。
排查Java CPU问题时,什么情况下优先使用Arthas而不是top+jstack?
当遇到以下场景时,优先使用Arthas:
遇到偶发性的CPU高占用(如每天凌晨出现),该如何排查?
可通过“定时快照+对比分析”解决:
jstack 进程PID >> /tmp/thread_$(date +%H%M).log
),覆盖问题高发时段;trace 包名.类名 方法名 -d 3600
监控1小时),记录方法执行频率和耗时,偶发性问题通常与定时任务、缓存失效或流量波动相关,通过多维度数据对比可快速定位根因。