当前位置: 代码网 > it编程>编程语言>Java > 线上Java项目CPU飙升、OOM排查与解决的整实战流程

线上Java项目CPU飙升、OOM排查与解决的整实战流程

2026年09月11日 Java 我要评论
前言凌晨两点,告警群炸了。cpu 打到 99%,或者服务直接 oom 挂掉,重启后没多久又复现。这种场景每个 java 后端都躲不掉,区别只在于你是手忙脚乱还是有条不紊。这篇文章把 cpu 飙升和 o

前言

凌晨两点,告警群炸了。cpu 打到 99%,或者服务直接 oom 挂掉,重启后没多久又复现。这种场景每个 java 后端都躲不掉,区别只在于你是手忙脚乱还是有条不紊。

这篇文章把 cpu 飙升和 oom 两条排查链路完整走一遍,每一步都给具体命令和操作,拿过去就能用。

一、总体排查思路

先建立全局视角,后面每个场景再深入细节。

告警触发
    │
    ├── cpu 飙升 → 找到占用 cpu 的进程 → 找到进程内占用 cpu 的线程
    │              → 定位到具体代码 → 分析根因 → 修复/止血
    │
    └── oom → 确认 oom 类型 → 拿到 heap dump / 分析内存分布
               → 找到内存泄漏点 → 分析根因 → 修复/止血

核心原则:先止血,再排查,最后根治。

止血手段包括:重启、降级、限流、回滚。不要为了排查问题让线上一直挂着——先让服务恢复,再留一台机器做分析。

二、cpu 飙升排查实战

step 1:找到占用 cpu 最高的进程

# 交互式查看,按 p 按 cpu 排序
top
# 或者一行命令直接拿
ps aux --sort=-%cpu | head -10

记下 pid,比如 18234

step 2:找到该进程内占用 cpu 最高的线程

# 查看进程内线程的 cpu 占用
top -hp 18234
# 或者
ps -lp 18234 cu | sort -nk3 -r | head -10

输出里会看到线程 id(lwp 列),比如 18256 占了 95% 的 cpu。

step 3:线程 id 转十六进制

java 线程 dump 里显示的线程 id 是十六进制,需要转换:

# 十进制 18256 → 十六进制
printf "%x\n" 18256
# 输出:4750

step 4:抓线程 dump,定位到具体线程

# 抓线程快照
jstack 18234 > /tmp/thread_dump.log
# 直接 grep 定位
jstack 18234 | grep -a 30 "nid=0x4750"

你会看到类似输出:

"http-nio-8080-exec-23" #45 daemon prio=5 os_prio=0 tid=0x00007f8a nid=0x4750 runnable
   java.lang.thread.state: runnable
        at com.example.service.orderservice.calculatediscount(orderservice.java:127)
        at com.example.service.orderservice.processorder(orderservice.java:88)
        at com.example.controller.ordercontroller.create(ordercontroller.java:45)

到这里已经定位到具体代码行了。 ​ 下一步就是分析这段代码为什么疯狂占 cpu。

step 5:常见 cpu 飙升根因

1. 死循环 / 无限循环

// ❌ 经典 case:边界条件写错
while (list.size() > 0) {
    item item = list.get(0);
    process(item);
    // 忘了 remove,或者 remove 的是另一个 list
}

2. 正则表达式灾难性回溯(redos)

// ❌ 这个正则遇到超长字符串会指数级回溯
pattern pattern = pattern.compile("(a+)+$");
pattern.matcher(userinput).matches();  // userinput = "aaaaaaaaaaaaaaaa!"

3. 频繁 gc(gc 线程本身占 cpu)

top 看到的是 gc 线程占 cpu,说明内存不够用,gc 在拼命干活。这时候问题本质是内存问题,跳到 oom 排查流程。

确认方法:

# 看 gc 情况
jstat -gcutil 18234 1000 10

如果 fgc 列在快速上涨、fgct 很大,说明 full gc 频繁,应用几乎在停顿。

4. 锁竞争 / 大量线程 blocked

线程 dump 里如果看到大量线程卡在同一个锁上:

java.lang.thread.state: blocked (on object monitor)
    - waiting to lock <0x00000000f8c3a0b0> (a java.lang.class)

说明存在严重的锁竞争,比如 synchronized 加在了大方法上,或者用了全局锁。

5. hashmap 并发修改导致链表成环(jdk 7)

多线程并发 put 导致链表成环,get 时死循环。升级 jdk 8+ 用 concurrenthashmap 解决。

step 6:实时观察(arthas 快速定位)

如果服务器上能装 arthas,排查效率会高很多:

# 启动 arthas,attach 到目标进程
curl -o https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 查看 cpu 占用最高的线程
dashboard
# 查看方法执行耗时
trace com.example.service.orderservice calculatediscount
# 查看方法调用次数和耗时
monitor -c 5 com.example.service.orderservice processorder
# 反编译线上代码确认
jad com.example.service.orderservice

三、oom 排查实战

step 1:确认 oom 类型

oom 不是一个错误,是一堆不同的错误。先看异常信息:

错误信息含义
java.lang.outofmemoryerror: java heap space堆内存不够,最常见
java.lang.outofmemoryerror: metaspace类元数据区满了,一般是动态生成类太多
java.lang.outofmemoryerror: gc overhead limit exceeded98% 时间在做 gc 但只回收了 <2% 内存
java.lang.outofmemoryerror: unable to create new native thread线程数达到系统上限
java.lang.outofmemoryerror: direct buffer memorynio 堆外内存溢出
java.lang.outofmemoryerror: requested array size exceeds vm limit试图分配超大数组

不同类型排查方向完全不同,下面以最常见的 堆 oom​ 为主线。

step 2:拿到 heap dump

最重要的一步。没有 dump 文件,oom 排查基本靠猜。

方式一:jvm 参数自动生成(推荐,提前配好)

-xx:+heapdumponoutofmemoryerror
-xx:heapdumppath=/data/logs/heapdump.hprof

这个一定要在 jvm 启动参数里配好,出问题时自动 dump,不额外增加运行时开销。

方式二:手动抓取(服务还活着时)

# jmap 抓 dump(会 stw,生产慎用,服务会暂停几秒到几十秒)
jmap -dump:format=b,file=/tmp/heap_dump.hprof 18234
# jdk 8+ 推荐用 jcmd,对服务影响更小
jcmd 18234 gc.run
jcmd 18234 gc.heap_dump /tmp/heap_dump.hprof

方式三:容器环境

# k8s 里 copy 出来
kubectl cp <pod-name>:/tmp/heap_dump.hprof ./heap_dump.hprof
# 或者用 ephemeral container
kubectl debug -it <pod-name> --image=openjdk:8 -- jcmd 1 gc.heap_dump /tmp/dump.hprof

step 3:分析 heap dump

工具选择

工具特点
eclipse mat最强大,功能全面,内存占用大
jprofiler商业,可视化好
jvisualvmjdk 自带,轻量,功能一般
arthas heapdump线上快速抓,配合分析

mat 分析流程

  1. 打开 dump 文件
  2. leak suspects report(mat 会自动给出疑似泄漏点)
  3. dominator tree(按对象保留内存大小排序)
  4. histogram(按类统计实例数和内存占用)

重点关注

histogram 里:
- 某个自定义对象实例数异常多(比如几十万个 orderdto)
- 占用内存排名前列的是 arraylist、hashmap、自己写的类

dominator tree 里:
- 找到占用内存最大的 gc root 引用链
- 顺着链看:是谁持有了这些对象没释放

step 4:常见 oom 根因

1. 集合类内存泄漏(最常见)

// ❌ 静态 map 只放不删
public class cacheholder {
    private static map<string, object> cache = new hashmap<>();
    
    public static void put(string key, object value) {
        cache.put(key, value);  // 永远不 remove,key 还不停增长
    }
}
// ❌ threadlocal 用完没 remove(线程池场景下尤其致命)
private static threadlocal<usercontext> contextholder = new threadlocal<>();

public void handlerequest() {
    contextholder.set(new usercontext());
    // 处理完没 remove,线程池线程复用,threadlocal 里的对象一直挂着
}
// ✅ 修复:try-finally 确保 remove
public void handlerequest() {
    contextholder.set(new usercontext());
    try {
        dowork();
    } finally {
        contextholder.remove();  // 关键
    }
}

2. 大对象 / 一次加载过多数据

// ❌ 一次性查全表
list<order> orders = ordermapper.selectall();  // 500 万条,直接 oom

// ✅ 分页或流式处理
pagehelper.startpage(1, 1000);
list<order> orders = ordermapper.selectbypage();

// 或者 mybatis 流式查询
@select("select * from orders")
@options(resultsettype = resultsettype.forward_only, fetchsize = 1000)
void selectall(resulthandler<order> handler);

3. 内存泄漏导致 metaspace oom

java.lang.outofmemoryerror: metaspace

常见原因:

  • 大量使用 cglib、asm、javaassist 动态生成类(如频繁创建代理对象)
  • groovy 脚本引擎重复解析脚本生成新类
  • 热部署框架(如 devtools)反复加载类

排查:

# 看类加载数量
jstat -class 18234
# 看 metaspace 使用
jstat -gc 18234

4. 堆外内存 oom

java.lang.outofmemoryerror: direct buffer memory

常见原因:

  • netty 的 bytebuf 没 release
  • nio bytebuffer.allocatedirect() 分配后没被 gc(directbytebuffer 依赖 cleaner 机制回收,不及时)

排查:

# 查看堆外内存使用
jcmd 18234 vm.native_memory summary
# 或者
pmap -x 18234 | sort -nk3 -r | head -20

step 5:不用 dump 的快速排查

如果 dump 文件太大(几十 gb)拿不下来,或者没法 dump,可以用这些手段:

# 实时看堆内存各区域使用
jstat -gcutil 18234 2000
# 输出示例:
#  s0     s1     e      o      m     ccs    ygc     ygct    fgc    fgct     gct
#  0.00  95.42  88.30  99.87  92.15  89.33  15234  320.5   188    4500.2   4820.7
#                                     ↑ 老年代 99.87%,快满了
# 看对象分布(不用 dump,开销小)
jmap -histo:live 18234 | head -20

jmap -histo:live 会触发 full gc,生产环境慎用,但信息量很大——直接告诉你内存里什么对象最多。

四、线程问题排查(blocked / 死锁)

有时候 cpu 不高,但服务响应极慢,大概率是线程卡住了。

抓线程 dump 分析

jstack 18234 > /tmp/thread_dump.log

死锁检测

jstack 18234 | grep -a 20 "deadlock"

或者 jstack 输出末尾会自动检测并报告死锁:

found one java-level deadlock:
=============================
"thread-1":
  waiting to lock monitor 0x00007f8a (object 0x00000000f8c3a0b0, a java.lang.object)
  which is held by "thread-2"
"thread-2":
  waiting to lock monitor 0x00007f8b (object 0x00000000f8c3a0c0, a java.lang.object)
  which is held by "thread-1"

大量线程 waiting / timed_waiting

"http-nio-8080-exec-50" #50 daemon prio=5 ... timed_waiting (parking)
    at sun.misc.unsafe.park(native method)
    at java.util.concurrent.locks.locksupport.parknanos(locksupport.java:215)
    at java.util.concurrent.threadpoolexecutor.gettask(threadpoolpoolexecutor.java:1067)

如果大量线程卡在等任务,说明线程池配置有问题或者队列满了。

五、容器化环境特殊注意事项

内存限制与 oom killer

容器里 java 进程被 kill,但日志里没有 oom 异常?大概率是被 linux oom killer 杀了。

# 查看系统日志
dmesg | grep -i "oom|killed"
# 或者
journalctl -k | grep -i oom

输出类似:

[timestamp] out of memory: kill process 18234 (java) score 800 or sacrifice child
[timestamp] killed process 18234 (java), uid 1000, total-vm:8192000kb, anon-rss:4096000kb

原因:jvm 没感知到容器内存限制,堆设大了,加上堆外内存、线程栈等,总内存超过容器 limit,被系统 kill。

解决

# jdk 8u191+ 支持容器内存感知
-xx:+usecontainersupport
-xx:maxrampercentage=75.0    # 堆占容器内存的 75%
-xx:initialrampercentage=50.0
# 或者明确指定
-xmx2g -xms2g

容器里 jstack / jmap 用不了

# 用 jcmd 替代
jcmd <pid> thread.print > thread_dump.log
jcmd <pid> gc.heap_dump /tmp/dump.hprof
# k8s 里用 nsenter
kubectl debug -it <pod-name> --image=busybox --target=<container-name>
nsenter -t 1 -m -p -n jcmd 1 thread.print

六、排查工具箱速查表

场景命令说明
找 cpu 最高进程top / ps aux --sort=-%cpu
找进程内 cpu 最高线程top -hp <pid>
线程 id 转十六进制printf "%x\n" <tid>
抓线程 dumpjstack <pid>
看 gc 状态jstat -gcutil <pid> 1000
看类加载jstat -class <pid>
看对象分布jmap -histo:live <pid>触发 full gc
抓 heap dumpjcmd <pid> gc.heap_dump <path>推荐
看堆外内存jcmd <pid> vm.native_memory summary
看系统 oomdmesg | grep oom
实时诊断arthas dashboard / trace / watch最强线上工具

七、事前预防(比事后排查更重要)

排查能力是保底,预防才是根本。以下配置建议全部加上:

# jvm 启动参数模板
java \
  -xms2g -xmx2g \
  -xx:+useg1gc \
  -xx:maxgcpausemillis=200 \
  -xx:+heapdumponoutofmemoryerror \
  -xx:heapdumppath=/data/logs/heapdump.hprof \
  -xx:+printgcdetails \
  -xx:+printgcdatestamps \
  -xloggc:/data/logs/gc.log \
  -xx:+usegclogfilerotation \
  -xx:numberofgclogfiles=5 \
  -xx:gclogfilesize=100m \
  -xx:+usecontainersupport \
  -xx:maxrampercentage=75.0 \
  -jar app.jar

再加上:

  • 监控告警:cpu > 80% 持续 2 分钟告警,full gc 频率突增告警
  • arthas 常驻:至少留一台机器能随时 attach
  • 压测:上线前做容量评估,知道极限在哪里
  • code review:重点审查集合类使用、threadlocal、线程池配置、大对象分配

八、一个完整实战案例

最后用一个真实案例串起来。

现象:服务 cpu 打到 100%,响应超时,重启后 10 分钟复现。

排查过程

# 1. top 找到 pid 18234
top
# cpu 99%,pid 18234
# 2. 找线程
top -hp 18234
# 线程 18256 占 98%
# 3. 转十六进制
printf "%x\n" 18256
# 4750
# 4. jstack
jstack 18234 | grep -a 20 "nid=0x4750"
# 定位到:com.example.orderservice.validateorder:203
# 5. 看代码
jad com.example.orderservice validateorder
# 发现正则:pattern.compile("^(a+)+$") 用于校验订单号
# 用户输入了一个超长字符串 "aaaaaaaaaaaa!",触发灾难性回溯
# 6. 止血:紧急回滚 + 修改正则为 ^[a-z]{1,32}$

根因:正则 redos,一行代码打挂整个服务。

修复:改用简单校验 + 长度限制,加输入校验白名单。

总结

排查线上问题的核心心法就三句话:

  1. 先止血再排查——重启、限流、回滚,别让问题扩大
  2. 工具比经验可靠——top、jstack、jstat、jcmd、arthas,数据说话
  3. 预防比排查重要——heapdumponoutofmemoryerror、gc 日志、监控告警,这些配置不花钱但能救命

到此这篇关于线上java项目cpu飙升、oom排查与解决的整实战流程的文章就介绍到这了,更多相关java项目cpu飙升与oom内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com