前言
线上告警:CPU 飙到 100%,服务响应超时。开终端、连服务器、top、jstack......
这一套流程对老手来说行云流水,但对新同学来说,每次遇到性能问题都可能是一次手忙脚乱------"先看什么?用什么命令?怎么看线程栈?"
这篇文章总结一套标准化的 Java 性能排障流程,遇到 CPU 飙升、内存泄漏、响应延迟问题时,按步骤来就行。
一、CPU 飙升排查(黄金流程)
第 1 步:找到高 CPU 进程
bash
# Linux
top
# 找到进程 PID,比如 Java 进程 pid=12345
输出示例:
perl
PID USER PR NI VIRT RES SHR S %CPU %MEM
12345 app 20 0 4.5g 1.2g 15m S 98.0 15.0 java
如果机器上只有一个 Java 进程,基本就是它了。
第 2 步:找到高 CPU 的线程
bash
top -H -p 12345
输出中会显示该进程下的所有线程及其 CPU 占用。找到 CPU 占用最高的线程,记下它的线程 ID(TID),比如 12399。
第 3 步:线程 ID 转 16 进制
bash
printf "%x\n" 12399
# 输出: 306f
这个 16 进制值 0x306f 是后续在 jstack 中定位线程的依据。
第 4 步:导出线程栈并定位
bash
jstack 12345 | grep -A 30 "0x306f"
输出会显示该线程的执行栈:
ruby
"http-nio-8080-exec-3" #45 daemon prio=5 os_prio=0 tid=0x00007f...
java.lang.Thread.State: RUNNABLE
at com.example.service.PremiumCalculator.calculate(PremiumCalculator.java:125)
at com.example.service.PremiumCalculator.lambda$batchCalculate$0(PremiumCalculator.java:78)
...
重点关注:RUNNABLE 状态 + 热点方法的行号------这就是 CPU 的"元凶"。
常见 CPU 飙升根因
| 根因类型 | 特征 |
|---|---|
| 死循环 (while true) | 线程栈看上去在一个循环方法里反复执行 |
| 频繁 GC (GC 线程抢占 CPU) | jstack 中有大量 GC 线程,配合 jstat -gcutil 确认 |
| 大量正则匹配 | 栈中能看到 Pattern.matcher 或 match() |
| 序列化/反序列化热点 | 栈中能看到 JSON/XML 解析相关调用 |
| 大对象频繁创建 | 栈中能看到循环创建新对象(StringBuilder、集合等) |
二、内存泄漏排查
第 1 步:确认内存泄漏
bash
# 查看 JVM 堆内存变化
jstat -gcutil <pid> 2000 10
S0 S1 E O M YGC YGCT FGC FGCT
0.00 96.32 88.5 92.3 95.2 452 3.245 38 12.56
0.00 95.89 90.2 93.1 95.1 453 3.248 39 13.02
关注 O(老年代)和 FGC(Full GC 次数)。老年代持续增长 + Full GC 越来越频繁 → 大概率内存泄漏。
第 2 步:堆转储(Heap Dump)
bash
# 方式一:运行时生成(服务不停)
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>
# 方式二:JVM 启动参数,OOM 时自动生成(推荐)
# -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/
第 3 步:分析 Dump 文件
用 MAT(Eclipse Memory Analyzer)或 VisualVM 打开 heap.hprof:
- 查看 Leak Suspects:工具自动推测泄漏点
- 查看 Dominator Tree:按保留堆大小排序,找出最大的对象
- 查看 GC Roots:确认对象被谁引用,为什么没有被回收
常见的泄漏模式
- ThreadLocal 未清理:线程池中的 ThreadLocal 没有在 finally 中 remove
- HashMap key 未实现 equals/hashCode:反复 put 相同逻辑 key 的不同对象,永远不会覆盖
- 静态集合作为缓存:static Map 只增不减
- ClassLoader 泄漏:热加载场景下的类卸载问题
三、响应延迟(RT 飙高)排查
第 1 步:区分是 CPU 问题还是 IO 等待
bash
# 查看 CPU 等待 IO 的比例
iostat -x 1
%iowait高 → IO 瓶颈%user高 → CPU 绑定型问题- 都不高 → 外部依赖(DB、Redis、外部 API)慢
第 2 步:外部依赖排查
bash
# 慢 SQL 排查
# 打开 MySQL 慢查询日志,或直接看 DBA 平台
# 查看 Redis 慢查询
SLOWLOG GET 10
# 查看外部 API 调用耗时:用 APM 工具(SkyWalking / Pinpoint)或业务日志中的耗时埋点
第 3 步:锁竞争排查
如果 jstack 中看到大量 BLOCKED 或 WAITING 状态的线程,可能是锁竞争:
bash
# 统计线程状态
jstack <pid> | grep "java.lang.Thread.State:" | sort | uniq -c
输出示例:
yaml
45 java.lang.Thread.State: RUNNABLE
15 java.lang.Thread.State: BLOCKED ← 锁竞争严重
10 java.lang.Thread.State: TIMED_WAITING
BLOCKED 多的线程,用 jstack 追它们的锁对象,看哪个锁被谁持有。
四:排障工具箱速查
| 场景 | 命令/工具 | 作用 |
|---|---|---|
| CPU 高 | top -H -p + jstack |
定位热点线程和方法 |
| 内存泄漏 | jstat -gcutil + jmap heap dump |
确认泄漏 + 分析根因 |
| GC 问题 | jstat -gcutil |
查看 GC 频率和耗时 |
| IO 瓶颈 | iostat -x |
查看 IO 等待比例 |
| 网络延迟 | ping / curl -w |
确认网络连通性和延迟 |
| 慢 SQL | MySQL SLOWLOG / DBA 平台 | 定位慢查询 |
| 锁竞争 | jstack |
查看线程状态分布 |
| 全链路追踪 | SkyWalking / Arthas | APM 监控 + 在线诊断 |
总结
性能排障的核心不是背命令,而是形成标准化的排查思路:
- CPU 高 → top → jstack → 热点方法
- 内存泄漏 → jstat → jmap heap dump → MAT 分析
- 响应慢 → 区分 CPU/IO/锁 → 逐一排查
遇到问题不慌,按这个流程走,99% 的性能问题都能在 15 分钟内定位到根因。
你常用的排障工具有哪些?欢迎补充你的压箱底技巧。