本文目录导读:

- 案例一:CPU 100% 飙升(死循环/正则回溯)
- 案例二:内存溢出(OOM)—— 线程创建过多导致无法分配栈内存
- 案例三:应用假死、端口不响应(死锁)
- 案例四:GC 频繁导致 CPU 高(内存泄漏初期)
- 案例五:连接数打满(数据库连接池 / HTTP连接池)
- 线上诊断三部曲
- 附录:诊断神兵利器 Arthas
Java线上诊断是每个高级开发必备的技能,下面整理了几个真实的经典案例,涵盖CPU飙升、内存泄漏、线程阻塞、以及GC频繁问题,并提供完整的排查思路与命令。
CPU 100% 飙升(死循环/正则回溯)
症状:线上告警,某台机器CPU使用率飙升至100%,接口响应变慢,但服务并未宕机。
排查思路:进程 -> 线程 -> 线程栈
操作步骤:
-
定位进程(如果是多实例,需要找到具体的那台机器):
top -c
查看CPU占用最高的
PID(假设为12345)。 -
定位线程(找到进程里最耗CPU的线程ID):
top -Hp 12345
找到CPU极高的线程
PID(假设为12346)。 -
转换线程ID为十六进制:
printf "%x\n" 12346 # 输出: 303a
-
查看线程栈(导出快照):
jstack 12345 | grep -A 30 "0x303a"
真实诊断:jstack 输出显示线程卡在 java.util.regex.Pattern$Curly.match 或 Pattern$GroupHead.match。
根因:代码中使用了类似 (a|aa)+b 这样的正则表达式,对长字符串进行匹配,触发了灾难性回溯(Catastrophic Backtracking),导致CPU空转。
解决方案:
- 临时方案:重启实例或紧急发布回滚。
- 修复方案:
- 优化正则表达式,消除嵌套量词(如
(a+)+)。 - 引入RE2/J等线性时间正则引擎。
- 如果必须使用Java正则,增加超时熔断机制。
- 优化正则表达式,消除嵌套量词(如
内存溢出(OOM)—— 线程创建过多导致无法分配栈内存
症状:服务突然宕机,日志中无具体业务报错,系统日志(dmesg)报错。
排查思路:系统日志 -> JVM参数 -> 创建线程的代码
操作步骤:
-
查看系统日志:
dmesg | tail -20 # 报错: java: page allocation failure: order:0, mode:0x0 # 或者: Out of memory: Kill process 12345 (java) score 989
注意:如果是操作系统层面的OOM Kill,
JVM日志里可能没有OutOfMemoryError堆栈。 -
查看JVM进程的线程数:
cat /proc/12345/status | grep Threads # 显示: Threads: 5000 # 发现线程数异常高(正常情况下几百以内)
-
打印线程快照,统计线程状态:
jstack 12345 | grep "java.lang.Thread.State" | sort | uniq -c | sort -rn # 发现大量: WAITING (parking)
-
查看磁盘空间:
df -h # 查看 /tmp 或日志目录是否写满,导致日志组件内部创建线程失败
根因:业务代码中存在死循环创建线程,或者使用了Executors.newCachedThreadPool()且未限制最大线程数,导致线程无限创建,最终撑爆内存或被OS杀死。
解决方案:
- 使用
ThreadPoolExecutor并显式指定有界队列和最大线程数。 - 排查为什么线程数会无限增长(可能是下游接口超时,导致任务堆积在队列外)。
应用假死、端口不响应(死锁)
症状:HTTP接口长时间无响应,但进程还在,CPU占用率很低(甚至为0%)。
排查思路:线程Dump -> 寻找 BLOCKED 线程 -> 寻找锁
操作步骤:
-
执行
jstack,注意需要多执行几次(间隔几秒),抓取2-3份快照。jstack 12345 > /tmp/jstack1.txt sleep 3 jstack 12345 > /tmp/jstack2.txt
-
检查死锁(jstack 自带检测): 在输出的最后面,通常会打印
Found one Java-level deadlock:字样。 -
如果没有打印,需要手动分析:
grep -B 5 -A 10 "locked" /tmp/jstack1.txt
寻找拥有锁但处于
WAITING的线程,以及等待该锁的BLOCKED线程。
真实案例:两个线程互相持有对方需要的锁:
Thread-A持有Lock-X,等待Lock-Y。Thread-B持有Lock-Y,等待Lock-X。
定位代码:检查是哪个类中的 synchronized 或 ReentrantLock。
解决方案:
- 临时:重启服务。
- 修复:调整加锁顺序,统一锁的获取顺序;使用
tryLock(long timeout, TimeUnit unit)代替直接lock(),避免无限期阻塞。
GC 频繁导致 CPU 高(内存泄漏初期)
症状:CPU使用率不高(20%-30%),但接口RT急剧上升,Full GC 频繁。
排查思路:堆内存分析 -> GC日志 -> MAT分析
操作步骤:
-
查看GC日志(如果JVM启动时未开启,需要加上重启):
jstat -gcutil 12345 1000 10 # 每隔1秒打印一次,连续10次 # 观察 FGC (Full GC次数) 是否快速增长,FGCT (时间) 是否很长
输出示例:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 0.00 45.00 98.00 95.00 97.00 500 10.0 100 20.0 30.0可以看到 Old区(O)占用98%,FGC已达100次。
-
导出堆转储(需要一定时间,最好在低峰期操作):
jmap -dump:live,format=b,file=/tmp/heap.hprof 12345 # 注意: 生产环境请谨慎使用,会暂停应用(STW)
-
使用MAT (Memory Analyzer Tool) 分析:
- 打开
heap.hprof。 - 查看 Leak Suspects Report。
- 查看 Dominator Tree 查找最大的对象。
- 打开
真实案例:分析发现 java.util.HashMap 占用了 80% 的堆内存。
根因定位:代码中使用 ThreadLocal 存储了用户信息,但请求结束后未执行 remove(),由于线程池中的线程复用,导致 ThreadLocalMap 中的 Entry 强引用一直在扩容,导致内存持久增长,最终引发频繁FullGC。
解决方案:
- 使用完
ThreadLocal后,在finally块中调用remove()。 - 如果使用
Netty等框架,注意其 FastThreadLocal 的清理机制。
连接数打满(数据库连接池 / HTTP连接池)
症状:服务日志大量报错:Connection is not available, request timed out after 30000ms。
排查思路:连接池监控 -> 慢SQL -> 未释放连接
操作步骤:
-
查看连接池指标(如果接入了Prometheus+Grafana,可直接看图表)。
-
查看MySQL中的连接状态(如果是DB连接池):
SHOW PROCESSLIST; -- 寻找大量处于 'Sleep' 状态的连接,且时间很长
-
代码排查: 检查
HikariCP/Druid的配置,maximum-pool-size是否过小?connection-timeout是否过短? -
分析Druid监控页面(如果是Druid),查看
ActiveCount和PoolingCount。
真实案例:业务代码使用了 RestTemplate 调用外部API,未设置连接池和超时时间,导致外部API变慢时,所有 HTTP 连接被占用,大量线程阻塞在获取连接上,进而导致数据库连接池被业务线程占满。
解决方案:
- 所有外部调用必须设置
connectTimeout和readTimeout。 - 使用带连接池的 HTTP 客户端(
OkHttp/Apache HttpClient),并合理设置最大连接数。 - 排查是否有数据库查询逻辑忘记
close()(虽然try-with-resources已普及,但手动获取Connection的场景仍需注意)。
线上诊断三部曲
遇到问题不要慌,按以下顺序操作:
-
看现象:
- CPU高 -> 查线程(案例一)。
- 内存高/GC频繁 -> 查堆(案例四)。
- 无响应/死锁 -> 查线程锁状态(案例三)。
- 依赖服务故障 -> 查连接池和超时(案例五)。
-
取快照:
jstack- 线程快照。jmap- 堆快照。jstat- GC统计。dmesg- 系统层日志。
-
深分析:
利用 MAT 分析大对象,利用 VisualVM 离线分析,或者用 Arthas(阿尔萨斯)在线热更新排查(推荐熟悉)。
附录:诊断神兵利器 Arthas
Arthas 是阿里开源的Java诊断工具,能解决上述很多需要重启才能解决的难题。
# 启动
java -jar arthas-boot.jar
# 在Arthas控制台内
# 1. 反编译线上代码,确认线上跑的是不是最新代码
jad com.example.MyService
# 2. 查看某个方法的入参和返回值(相当于动态打印日志)
watch com.example.MyService getUserInfo '{params[0], returnObj}' -x 3
# 3. 查看方法耗时和各步骤耗时(火焰图)
trace com.example.MyService getUserInfo
# 4. 查看线程CPU占用
thread -n 3