Java应用服务器CPU飙升不是玄学,九成情况下是线程阻塞、GC循环、连接池打满、代码热点或资源配置这五个环节里出了某个可定位的bug,按照“先看全局、再抓线程、后查GC、最后调参数”的顺序,多数问题能在半小时内找到根因并解决。
先别急着加机器,确认CPU是被谁吃掉的
很多人一看到监控里CPU红了,第一反应是扩容或重启,但重启只能掩盖问题,十分钟后故障照旧,正确的第一步是把“CPU占用高”这个模糊现象,拆解成“哪个进程、哪些线程、在做什么”。
用top命令锁定进程PID
登录服务器执行:
top -c
按P键按CPU使用率排序,找到排名第一的Java进程,记下PID,这一步大概率能定位到一个孤儿进程或异常线程组,
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 23456 root 20 0 12.4g 1.6g 1.3g S 198.0 5.2 43:21.67 java -Xmx2g ...
注意CPU占用超过100%说明进程内部有多核线程在并发燃烧,CPU核数越多,这个数值可以越高。
进入进程内部看线程
只知道进程不够,下一步用top的线程模式:
top -Hp 23456
再次按P排序,这次看到的是线程ID(TID),是十六进制的nid对应的十进制数,记下最耗CPU的几个线程ID,比如TID=34821、34822。
将线程ID转换为十六进制
执行:
printf "%x\n" 34821
得到十六进制值比如0x8805,这个值是用来在jstack快照中匹配线索的钥匙。
抓取jstack线程快照
jstack 23456 > jvm_stack.log
在日志中搜索0x8805,就能看到对应线程的具体状态和调用栈,这一条命令能回答多数新手最关心的“Java应用服务器CPU飙高怎么排查”这个问题排查的核心不是调参,而是定位线程在干什么。
Java线程卡死与锁竞争,CPU高热的主要来源之一
业内专家指出,生产环境中遇到的CPU飙升,相当一部分不是GC问题,而是线程在错误地空转或死等。
三种典型线程状态对比
| 线程状态 | 含义 | CPU表现 |
|---|---|---|
| RUNNABLE | 正在执行或等待CPU调度 | 高,正常业务高峰也如此 |
| BLOCKED | 等待进入synchronized同步块 | 中等,锁竞争激烈时飙升 |
| WAITING/TIMED_WAITING | 主动挂起等待唤醒 | 低,但间接拖垮整体吞吐 |
死循环与活锁,CPU计算型异常的典型表现
在jstack日志中,如果发现某个线程长时间处于RUNNABLE且调用栈停留在业务代码段,往往对应以下几种常见情况:
- 循环缺少退出条件,例如
while(true)内没有按预期break - 递归深度失控,栈帧疯狂入栈出栈
- 通过
java.util.logging或log4j开启Debug级日志,在大流量下执行字符拼接和IO写入 - 正则表达式处理恶意长文本时产生灾难性回溯
锁竞争伪造的“高CPU”
另一种常见场景是线程状态显示BLOCKED但整体CPU依然很高,大量线程在争抢同一把锁,频繁触发操作系统的上下文切换和内核态调度,此时jstack中能看到大量线程阻塞在同一个Monitor地址附近。
面对这些情况,处理方式不是盲目调整线程池大小,而是:
- 用jstack连续抓取3-5次快照(间隔3-5秒),确认同一个线程是否始终停在同一个位置
- 如果调用栈一致,说明该线程在死循环或长时间持锁,修复代码逻辑
- 如果调用栈变化但线程状态不健康,排查锁粒度,考虑改用
ReentrantReadWriteLock或LongAdder替代synchronized和AtomicLong减少竞争
GC线程消耗CPU,Java特有却常被忽略的元凶
网上检索“Java CPU占用过高怎么优化”时,大量帖子会指向GC调优,这个方向没有错,但不能一上来就改堆大小,先确认GC是不是主因。
用jstat快速判断GC压力
拿到PID后,执行:
jstat -gcutil 23456 1000 5
每隔1秒输出一次,连续5次,观察FGC(Full GC次数)和FGCT(Full GC耗时)的变化,如果FGC在短短几秒内增加数次,且FGCT持续上升,说明Full GC正在密集发生,GC线程本身就足以打满多核CPU。
另一个指标是YGC,新生代回收过于频繁同样会让CPU居高不下,如果输出显示:
FGC增长率过快,说明老年代空间被迅速填满YGC间隔极短,说明对象分配速率异常,可能存在大对象或无限创建短期对象
常见GC异常的根因
多数情况下GC导致CPU飙高,源于这几类问题:
- 堆内存设置过小,对象频繁晋升到老年代,触发连续Full GC
- 代码中创建了大体积的缓存集合且没有过期淘汰策略
- 被第三方接口或大查询一次性加载了海量数据
- JVM参数中使用了不恰当的垃圾收集器或错误的区域比例
分场景处置策略
堆内存明显偏小的,按服务器物理内存的50%-60%调整-Xms和-Xmx为相同值,避免运行时动态扩容带来的额外开销。
对象分配速率异常的,用jmap -histo:live 23456打印存活对象直方图,排查占用最高的类是否来自业务代码中的全局集合或缓存,若发现单个业务对象占据数GB内存且有持续增长趋势,优先修业务代码而不是调GC。
GC频繁但堆内存仍有空间的,可能问题出在元空间(Metaspace)加载了大量类或使用了CGLib动态生成代理类,执行jstat -gcmetacapacity确认元空间使用率。
行业共识认为,GC调优的终点是让系统在稳定负载下保持YGC平缓、Full GC几乎不发生,而不是追求某个固定的回收器或堆大小。
连接池与数据库交互,另一个常被忽视的CPU消耗源
有一种场景,Java应用自身工作线程CPU不高,但整体进程CPU居高不下,而且jstack中大量线程卡在数据库驱动的等待IO或socketRead上,这种情况往往与糟糕的数据库交互模式有关。
连接池配置不当引发线程空转
当连接池最大连接数设置过小,业务请求并发一上来,所有线程都在等待获取连接,如果连接获取超时时间设置得较长,容易出现大量线程BLOCKED,同时连接池内部的创建和销毁逻辑反复空转,消耗CPU。
检查Druid或HikariCP的监控统计,分析以下指标:
- 活跃连接数是否长期接近最大连接数上限
- 等待获取连接的时间是否持续偏高
- 创建连接次数是否远超预期
如果确认是连接池打满,不是无脑调大maxPoolSize,而是先分析连接的释放和持有时长,一条表面正常的慢SQL,在高并发下会把连接池占满,此时调大连接池只会让数据库CPU继续飙升。
慢SQL与全表扫描的连锁效应
在业务侧加入MySQL慢查询日志或调用链追踪,检查是否存在:
- 大表未命中索引的查询,频繁触发全表扫描
- N+1查询,循环内逐条访问数据库
- 在
for循环中调用远程服务或数据库操作,导致大量短连接请求
针对这类问题,应用侧加@Transactional或批量合并查询往往比调数据库参数更直接,捕获SQL执行耗时后,优先重写SQL和补充索引,必要时引入本地缓存减少重复查询。
JVM参数与业务代码的实操调优清单
如果前端排查后线程、GC和连接池都无明显异常,CPU消耗分布较为均匀,则进入最后一轮系统级调优,这里给出一份可照做的操作清单。
查看当前JVM参数生效情况
jinfo -flags 23456
确认堆内存、GC收集器、JIT编译线程数等关键参数是否合理,例如-XX:CICompilerCount决定了JIT编译线程数量,默认值为2或CPU核数相关值,但在容器环境中可能被误设过高,导致编译线程吃满CPU。
线程池配置的合理尺度
常见框架的线程池默认值通常偏保守或偏激进,两种情况都会造成CPU使用率异常:
| 场景 | 线程池过小 | 线程池过大 |
|---|---|---|
| 表现 | 请求排队等待,CPU利用率低但延迟高 | 线程频繁切换,内核态CPU飙升 |
| 处置 | 逐步调大核心线程数 | 减少核心线程,加入队列限流 |
使用ThreadPoolExecutor时关注queueCapacity和rejectedExecutionHandler,让线程数保持在一个合理的梯度上,一般经验值是CPU核数 (1 + 平均IO等待时间 / 平均CPU计算时间),但具体数值要通过压测逐步逼近。
代码层面的常见热点
排查到最后,一定比例的问题会回归到最朴素的代码写法上:
- 大字符串循环拼接改为
StringBuilder - 避免在循环内调用
System.currentTimeMillis()或UUID.randomUUID()大批量生成耗时对象 - 关闭不必要的日志级别的输出,特别是
debug级别 - 序列化场景中优先使用
Protobuf或Kryo,减少JSON序列化带来的CPU开销 - 对于热点小数据,使用
Caffeine本地缓存代替远端Redis每次访问
这一阶段不追求一步到位,每次调整后观察CPU曲线变化,逐步逼近理想状态。
常见问题解答
Java应用服务器CPU高但jstack看不到业务线程,是怎么回事?
如果jstack中业务线程都处于空闲状态,但CPU依旧居高不下,优先排查JIT编译线程(C2 CompilerThread)和GC线程,执行top -Hp PID观察这两个线程的CPU消耗,必要时检查-XX:CICompilerCount参数,以及JIT是否在频繁反优化重编译热点方法。
Java应用服务器CPU飙高只能重启吗,有没有不停机的手段?
多数CPU飙升问题可以不停机处理,先用jstack连续抓取快照判断问题来源,如果定位到死循环或错误线程,可通过动态开关降级或调整日志级别暂缓压力,再编排灰度发布修复代码,只有GC失控到Full GC已经导致长时间停顿、无法建立新连接时,才考虑重启作为临时止血。
容器环境下Java应用CPU监控指标异常,有没有特殊注意事项?
容器环境中,应用看到的CPU核数和宿主机可能不一致,Runtime.getRuntime().availableProcessors()返回的数值受-XX:ActiveProcessorCount和cgroup配置影响,当容器实际分配核数较小但与宿主机共享CPU时,top和jstack的观测路径会有所不同,需要结合容器监控平台的CPU限额和宿主机整体负载一起判断,避免将宿主机上的其他应用干扰误判为自身问题。
Java应用服务器降CPU与其说是一门技术,不如说是一个循证的过程,任何结论都必须建立在连续抓取的线程快照、准确的GC日志和真实的连接池指标之上,顺着数据找代码,而不是顺着感觉调参数,这才是解决CPU消耗问题的根本路径。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/791862.html


