生产环境 cpu 飙高,如何排查
生产环境 cpu 飙高,如何排查
🎯 记住核心:ps -mp 找线程 → printf 转16进制 → jstack 看堆栈 → 定位代码!
一、标准排查流程(5步定位法)
第1步:确认是哪个进程占用CPU高(10秒)
# 方法1:查看所有进程CPU占用(按CPU降序)
ps aux --sort=-%cpu | head -10# 方法2:实时监控
top
# 按 Shift + P(按CPU排序)# 输出示例:
# USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
# app 12345 187.5 35.2 8234568 2345678 ? Sl 10:23 125:34 java -jar myapp.jar
记录关键信息:
- 进程PID:12345
- CPU占用:187.5%(多核系统,单核100%,4核400%)
- 进程命令:java -jar myapp.jar
第2步:找出该进程中CPU占用高的线程(30秒)
# 使用ps命令查看进程的所有线程,按CPU降序
ps -mp <PID> -o THREAD,tid,time,%cpu | sort -k4 -rn | head -10# 实际命令示例
ps -mp 12345 -o THREAD,tid,time,%cpu | sort -k4 -rn | head -10# 输出示例:
# USER %CPU PRI SCNT WCHAN USER SYSTEM TID TIME
# app 87.3 19 - - - - 23456 0:23:15 ← 热点线程
# app 45.2 19 - - - - 23457 0:18:42
# app 5.1 19 - - - - 23458 0:00:12
或使用top命令查看线程:
top -H -p 12345
# -H 显示线程
# -p 指定进程PID
记录高CPU的线程TID:
- TID(线程ID):23456
- CPU占用:87.3%
第3步:TID转换为16进制(5秒)
# jstack导出的堆栈中,线程nid是16进制格式,需要转换
printf "0x%x\n" <TID># 实际命令示例
printf "0x%x\n" 23456
# 输出:0x5ba0
记录16进制TID:0x5ba0
第4步:导出线程堆栈(1分钟)
# 导出jstack堆栈到文件
jstack <PID> > thread_dump_$(date +%Y%m%d_%H%M%S).txt# 实际命令示例
jstack 12345 > thread_dump_20250105_143022.txt# 建议连续导出3次(间隔3秒),对比分析
jstack 12345 > thread_dump_1.txt && sleep 3 && \
jstack 12345 > thread_dump_2.txt && sleep 3 && \
jstack 12345 > thread_dump_3.txt
第5步:搜索热点线程堆栈并定位代码(2分钟)
# 使用grep搜索对应的线程(用16进制nid)
grep -A 50 "nid=0x5ba0" thread_dump_1.txt# 输出示例:
"http-nio-8080-exec-23" #45 daemon prio=5 os_prio=0 tid=0x00007f8b3c012800 nid=0x5ba0 runnablejava.lang.Thread.State: RUNNABLEat com.example.service.OrderService.checkOrderStatus(OrderService.java:156)at com.example.service.OrderService.processOrder(OrderService.java:89)at com.example.controller.OrderController.createOrder(OrderController.java:45)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)...
关键信息提取:
- 线程名:http-nio-8080-exec-23
- 线程状态:RUNNABLE(正在运行)
- 问题代码:OrderService.java:156
- 方法名:checkOrderStatus()
二、快速判断问题类型
| 堆栈特征 | 问题类型 |
|---|---|
| RUNNABLE + 同一方法反复出现 | 死循环 |
| Pattern$Loop.match | 正则回溯 |
| GC.task_thread | 频繁Full GC |
| BLOCKED + waiting to lock | 锁竞争 |
| Socket/MySQL相关 | IO阻塞/慢SQL |
三、常见问题案例与修复
案例1:死循环空转(CPU 100%)
堆栈特征:
at com.example.service.MessageConsumer.consume(MessageConsumer.java:45)
at com.example.service.MessageConsumer.consume(MessageConsumer.java:45) // 重复
at com.example.service.MessageConsumer.consume(MessageConsumer.java:45) // 重复
问题代码:
// ❌ 错误代码
while (true) {Message msg = queue.poll(); // 非阻塞,立即返回if (msg == null) {continue; // 空转!CPU 100%}processMessage(msg);
}
修复方案:
// ✅ 正确代码
while (!Thread.interrupted()) {Message msg = queue.poll(100, TimeUnit.MILLISECONDS); // 阻塞等待if (msg != null) {processMessage(msg);}
}
案例2:正则表达式灾难性回溯
堆栈特征:
at java.util.regex.Pattern$Loop.match(Pattern.java:4785)
at java.util.regex.Pattern$GroupTail.match(Pattern.java:4717)
at com.example.util.EmailValidator.validate(EmailValidator.java:23)
问题代码:
// ❌ 危险正则(嵌套量词)
Pattern pattern = Pattern.compile("^([a-zA-Z0-9]+)*@([a-zA-Z0-9]+)*\\.com$");
pattern.matcher("aaaaaaaaaaaaaaaaaaaaX").matches(); // 指数级回溯
修复方案:
// ✅ 简化正则
Pattern pattern = Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");// ✅ 加长度限制
if (email.length() > 100) {return false;
}
案例3:频繁Full GC导致CPU高
判断方法:
# 查看GC统计
jstat -gc <PID> 1000 10# 关键指标:
# FGC - Full GC次数(快速增长)
# FGCT - Full GC总耗时(持续增加)
# OU - Old区使用量
# OC - Old区容量# 示例输出:
# S0C S1C S0U S1U EC EU OC OU FGC FGCT
# 10240 10240 0 8192 81920 45000 204800 198000 89 156.7
# ^^^^^^ ^^ ^^^^^
# Old区97% Full GC 89次
修复方案:
- 增大堆内存:-Xms8g -Xmx8g
- 排查内存泄漏:jmap -histo:live | head -20
- 优化代码:减少大对象创建、使用对象池
案例4:synchronized锁竞争
堆栈特征:
"http-nio-8080-exec-25" waiting for monitor entryjava.lang.Thread.State: BLOCKED (on object monitor)at com.example.cache.CacheService.get(CacheService.java:45)- waiting to lock <0x00000000e1234567> (a java.util.HashMap)- locked by "http-nio-8080-exec-12" // 被其他线程持有
问题代码:
// ❌ 锁粒度太大
public synchronized User getUser(Long id) {return userCache.get(id);
}
修复方案:
// ✅ 使用并发集合
private ConcurrentHashMap<Long, User> userCache = new ConcurrentHashMap<>();public User getUser(Long id) {return userCache.get(id); // 无锁读
}
四、一键排查脚本(推荐使用)
#!/bin/bash
# 保存为 cpu_diagnosis.shecho "========== CPU飙高诊断 =========="# 1. 找到CPU最高的Java进程
PID=$(ps aux --sort=-%cpu | grep java | grep -v grep | head -1 | awk '{print $2}')
echo "Java进程PID: $PID"# 2. 显示CPU类型
echo -e "\n=== CPU类型分布 ==="
top -bn1 | grep "Cpu(s)" | awk '{print "User:" $2, "System:" $4, "IOWait:" $10}'# 3. 显示Top 5 CPU线程
echo -e "\n=== Top 5 CPU线程 ==="
echo "TID %CPU HEX_TID"
ps -mp $PID -o THREAD,tid,time,%cpu | sort -k4 -rn | awk 'NR>1 && NR<=6 {tid=$2; cpu=$4;if (tid != "-") {cmd="printf \"0x%x\" " tid;cmd | getline hex;close(cmd);printf "%-10s %-7s %s\n", tid, cpu"%", hex;}
}'# 4. 导出堆栈
echo -e "\n=== 导出堆栈 ==="
DUMP_FILE="thread_dump_$(date +%H%M%S).txt"
jstack $PID > $DUMP_FILE
echo "堆栈已导出: $DUMP_FILE"# 5. 提示下一步
echo -e "\n=== 下一步 ==="
echo "使用以下命令查看热点线程堆栈:"
echo "grep -A 50 'nid=0x<HEX_TID>' $DUMP_FILE"echo -e "\n========== 诊断完成 =========="
使用方法:
# 1. 保存脚本
vim cpu_diagnosis.sh
# 粘贴上述脚本内容# 2. 添加执行权限
chmod +x cpu_diagnosis.sh# 3. 执行(需要root或应用用户权限)
sudo ./cpu_diagnosis.sh
复杂版:
#!/bin/bash
# cpu_diagnosis.sh - CPU飙高自动诊断脚本set -e# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Colorecho -e "${GREEN}========================================${NC}"
echo -e "${GREEN} CPU飙高问题自动诊断工具 v1.0${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""# 1. 找到CPU最高的Java进程
echo -e "${YELLOW}[Step 1] 定位问题进程...${NC}"
JAVA_PID=$(ps aux --sort=-%cpu | grep java | grep -v grep | head -1 | awk '{print $2}')if [ -z "$JAVA_PID" ]; thenecho -e "${RED}错误:未找到Java进程${NC}"exit 1
fiJAVA_CMD=$(ps -p $JAVA_PID -o cmd --no-headers | cut -c1-80)
CPU_USAGE=$(ps -p $JAVA_PID -o %cpu --no-headers)echo -e " ${GREEN}✓${NC} 目标进程: PID=${JAVA_PID}"
echo -e " ${GREEN}✓${NC} CPU使用率: ${CPU_USAGE}%"
echo -e " ${GREEN}✓${NC} 命令: $JAVA_CMD"
echo ""# 2. 显示CPU类型分布
echo -e "${YELLOW}[Step 2] 分析CPU类型分布...${NC}"
top -bn1 | grep "Cpu(s)" | \
awk -v red="$RED" -v green="$GREEN" -v yellow="$YELLOW" -v nc="$NC" '{us=$2; sy=$4; wa=$10; id=$8;printf " 用户态(us): " us " ";if (us+0 > 80) printf red "⚠️ 高" nc; else printf green "✓" nc;printf "\n";printf " 系统态(sy): " sy " ";if (sy+0 > 30) printf red "⚠️ 高" nc; else printf green "✓" nc;printf "\n";printf " IO等待(wa): " wa " ";if (wa+0 > 20) printf red "⚠️ 高" nc; else printf green "✓" nc;printf "\n";printf " 空闲(id): " id "\n";
}'
echo ""# 3. 找出Top 5 CPU线程
echo -e "${YELLOW}[Step 3] 定位热点线程...${NC}"
echo " TID CPU% HEX_TID"
echo " --------------------------------"
ps -mp $JAVA_PID -o THREAD,tid,time,%cpu --sort=-%cpu | awk 'NR>1 && NR<=6 {tid=$2; cpu=$4;if (tid != "-") {cmd="printf \"0x%x\" " tid;cmd | getline hex;close(cmd);printf " %-10s %-7s %s\n", tid, cpu"%", hex;}
}'
echo ""# 4. 快速检查GC情况
echo -e "${YELLOW}[Step 4] 检查GC状态...${NC}"
if command -v jstat &> /dev/null; thenjstat -gc $JAVA_PID 1000 3 2>/dev/null | tail -2 | \awk 'NR==1{ou=$7; oc=$8; fgc=$14; fgct=$15;usage=ou/oc*100;printf " Old区使用率: %.1f%%", usage;if (usage > 95) printf " " red "⚠️ 接近满" nc;printf "\n";printf " Full GC次数: %s (总耗时: %ss)\n", fgc, fgct;}'
elseecho -e " ${YELLOW}⚠ jstat未安装,跳过GC检查${NC}"
fi
echo ""# 5. 导出线程堆栈
echo -e "${YELLOW}[Step 5] 导出线程堆栈...${NC}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DUMP_DIR="cpu_issue_$TIMESTAMP"
mkdir -p $DUMP_DIRif command -v jstack &> /dev/null; thenfor i in {1..3}; dojstack $JAVA_PID > "$DUMP_DIR/thread_dump_$i.txt" 2>/dev/nullecho -e " ${GREEN}✓${NC} thread_dump_$i.txt"sleep 2done# 统计线程状态echo ""echo " 线程状态分布:"jstack $JAVA_PID 2>/dev/null | grep "java.lang.Thread.State" | sort | uniq -c | \while read count state; doprintf " %-30s: %s\n" "$state" "$count"done
elseecho -e " ${RED}✗ jstack未安装${NC}"
fi
echo ""# 6. 系统信息
echo -e "${YELLOW}[Step 6] 导出系统信息...${NC}"
top -H -p $JAVA_PID -b -n 1 > "$DUMP_DIR/top.txt" 2>/dev/null && \echo -e " ${GREEN}✓${NC} top.txt"
ps -mp $JAVA_PID -o THREAD,tid,time,%cpu --sort=-%cpu > "$DUMP_DIR/ps_threads.txt" 2>/dev/null && \echo -e " ${GREEN}✓${NC} ps_threads.txt"
vmstat 1 5 > "$DUMP_DIR/vmstat.txt" 2>/dev/null && \echo -e " ${GREEN}✓${NC} vmstat.txt"
echo ""# 7. 打包
echo -e "${YELLOW}[Step 7] 打包诊断文件...${NC}"
tar czf "${DUMP_DIR}.tar.gz" $DUMP_DIR 2>/dev/null
echo -e " ${GREEN}✓${NC} ${DUMP_DIR}.tar.gz"
echo ""# 8. 生成分析建议
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} 诊断完成!${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo -e "${YELLOW}📋 下一步操作:${NC}"
echo ""
echo "1. 查看热点线程堆栈:"
echo " grep -A 50 'nid=0x<HEX_TID>' $DUMP_DIR/thread_dump_1.txt"
echo ""
echo "2. 搜索常见问题特征:"
echo " grep -r 'Pattern\$Loop' $DUMP_DIR/"
echo " grep -r 'BLOCKED' $DUMP_DIR/"
echo ""
echo "3. 如果怀疑GC问题:"
echo " jmap -heap $JAVA_PID"
echo " jmap -histo:live $JAVA_PID | head -20"
echo ""
echo "4. 生成火焰图(推荐):"
echo " java -jar arthas-boot.jar"
echo " profiler start && sleep 30 && profiler stop --format html"
echo ""
echo -e "${YELLOW}📦 诊断文件已保存到: ${DUMP_DIR}.tar.gz${NC}"
五、高级工具:Arthas(强烈推荐)
Arthas是阿里开源的Java诊断神器,无需修改代码、无需重启应用。
安装使用:
# 1. 下载
wget https://arthas.aliyun.com/arthas-boot.jar# 2. 启动(自动列出Java进程,选择目标进程)
java -jar arthas-boot.jar# 3. 核心命令
# 查看CPU最高的3个线程(自动显示堆栈)
thread -n 3# 查看指定线程
thread <tid># 查找死锁
thread -b# 实时监控面板
dashboard# 生成火焰图(最直观)
profiler start
# 等待30-60秒
profiler stop --format html
# 生成 arthas-output/profiler-timestamp.html
Arthas优势:
✅ 自动找到高CPU线程
✅ 自动显示完整堆栈
✅ 生成火焰图(可视化)
✅ 无需TID转16进制
✅ 实时监控
六、预防措施
- 代码层面:
✅ 循环优化□ 避免while(true)空转,使用阻塞等待□ 避免在循环内创建对象(如Pattern.compile)□ 使用懒加载,按需计算✅ 正则表达式□ 避免嵌套量词:(a+)+、(a*)*□ 输入长度限制(如<100字符)□ 复杂正则添加超时保护✅ 集合操作□ 并发场景使用ConcurrentHashMap□ 指定集合初始容量,避免扩容□ 大集合分批处理✅ 数据库□ 避免N+1查询,使用JOIN□ 使用分页查询(LIMIT)□ 添加合适索引□ 使用连接池✅ 并发控制□ 锁粒度最小化□ 使用ConcurrentHashMap等并发集合□ 避免在synchronized内调用外部方法□ 线程池配置合理:核心数=CPU核数*2✅ 资源管理□ 及时关闭资源(try-with-resources)□ 使用对象池(数据库连接、HTTP连接)□ 避免内存泄漏(静态集合、ThreadLocal)
- JVM参数:
# 生产环境推荐配置
java -server \-Xms8g -Xmx8g \ # 堆内存初始=最大-Xmn2g \ # 年轻代2GB-XX:MetaspaceSize=256m \ # 元空间-XX:MaxMetaspaceSize=512m \ # 元空间最大值-XX:+UseG1GC \ # G1垃圾收集器-XX:MaxGCPauseMillis=200 \ # GC暂停目标200ms-XX:G1HeapRegionSize=16m \ # G1 Region大小-XX:+HeapDumpOnOutOfMemoryError \ # OOM时自动Dump-XX:HeapDumpPath=/data/logs/heap_dump.hprof \-XX:+PrintGCDetails \ # 打印GC详情-XX:+PrintGCDateStamps \ # 打印GC时间戳-Xloggc:/data/logs/gc_%p_%t.log \ # GC日志路径-XX:+UseGCLogFileRotation \ # GC日志滚动-XX:NumberOfGCLogFiles=10 \ # 保留10个GC日志-XX:GCLogFileSize=100M \ # 每个日志100MB-XX:+UnlockDiagnosticVMOptions \ # 解锁诊断选项-XX:+PrintSafepointStatistics \ # 打印安全点统计-XX:PrintSafepointStatisticsCount=1 \ # 每次都打印-jar app.jar
八、快速参考卡
CPU飙高排查5步速查表
- 1️⃣ ps aux --sort=-%cpu → 找进程PID
- 2️⃣ ps -mp -o THREAD → 找线程TID
- 3️⃣ printf “0x%x” → TID转16进制
- 4️⃣ jstack → 导出堆栈
- 5️⃣ grep “nid=0x” → 定位代码
快捷命令:
- ps -mp -o THREAD,tid,%cpu | sort -k4 -rn
- jstack | grep -A 50 “nid=0x”
神器:Arthas
- java -jar arthas-boot.jar
- thread -n 3
- profiler start && profiler stop
经验总结:
- 90%的CPU飙高都是代码问题(死循环、正则、GC)
- 堆栈连续导出3次对比分析更准确
- 保留现场比快速重启更重要
- 预防胜于治疗,代码审查+监控告警
最后忠告:线上问题,先保留现场,再重启应用!
