Java 线上排查标准手册:CPU 飙高、内存泄漏、接口慢,jstack/jmap/jstat 命令速查

目录


一、前置准备:看一眼 top 确定方向

所有排查的第一步——确认问题类型:

# 找到 Java 进程 PID
jps -l
# 或
pgrep -f java

# 看 CPU 和内存
top -p <java_pid>
  PID USER  %CPU %MEM    TIME+  COMMAND
12345 root  95.2  8.3  12:30.15 java

决策树:

top 看到什么 → 走哪条路径
─────────────────────────────────
%CPU > 80% 且持续   → 第二章「CPU 飙高」
%MEM 持续上涨       → 第三章「内存泄漏」
%CPU/%MEM 正常但慢  → 第四章「接口慢」
线程数异常增加      → 第六章「线程异常」

二、CPU 飙高排查

2.1 找到吃 CPU 的线程

# -H:显示线程级别
# -p:指定进程
top -Hp <java_pid>

输出示例:

  PID USER  %CPU %MEM    TIME+  COMMAND
12399 root  92.1  0.1   5:30.15 java    ← 这个线程吃了 92% CPU
12345 root   1.2  8.3  12:30.15 java
12401 root   0.5  0.1   0:10.22 java

记下最忙线程的 PID(这里是 12399)。

2.2 PID 转十六进制

printf "%x\n" 12399
# 输出:306f

2.3 jstack 定位代码行

# 打印线程栈,grep 过滤目标线程
jstack <java_pid> | grep -A 30 "0x306f"

输出解读:

"http-nio-8080-exec-5" #42 daemon prio=5 os_prio=0 tid=0x... nid=0x306f runnable
   java.lang.Thread.State: RUNNABLE
        at com.example.OrderService.queryOrders(OrderService.java:87)
        at com.example.OrderController.list(OrderController.java:23)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:...)
        ...
栈顶出现什么 含义 下一步
你的业务代码 + 行号 死循环或密集计算 审查该行代码逻辑
HashMap.get() / ArrayList.grow() 集合操作不当 检查数据量或 hash 冲突
GC 相关类 内存不足触发频繁 GC 转到第三章
SocketInputStream.read() 等待网络 I/O 其实不是 CPU 问题,看第四章

2.4 持续采样确认

# 每 3 秒采样一次,共 5 次,看是否卡在同一行
for i in $(seq 1 5); do
  echo "=== Sample $i ==="
  jstack <java_pid> | grep -A 5 "0x306f"
  sleep 3
done

5 次都在同一行 → 死循环或密集计算,改代码。
每次在不同行 → 高并发请求打进来,考虑扩容或限流。


三、内存泄漏 / OOM 排查

3.1 jstat 看 GC 概况

# 每 1 秒输出一次,共 10 次
jstat -gc <java_pid> 1000 10

输出列说明:

 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU
 Survivor区   Survivor区      Eden区    Eden区    老年代容量   老年代使用量  元空间
 容量       使用量           容量     使用量

重点关注:

jstat -gc <java_pid> 1000 5 | awk '
NR==1 {print "老年代使用率 | YGC | YGCT | FGC | FGCT | GCT"}
NR>1 {
  ou=$8; oc=$7;
  rate=(oc>0 ? ou/oc*100 : 0);
  printf "  %.1f%%       | %4d | %5.2f | %3d | %5.2f | %5.2f\n", rate,$9,$10,$11,$12,$13
}'
指标 正常值 异常值 含义
老年代使用率 (OU/OC) < 70% > 90% 即将 OOM
FGC(Full GC 次数) 每小时 < 5 每几分钟一次 内存泄漏导致频繁 GC
FGCT(Full GC 耗时) < 2s > 10s GC 停顿严重影响响应
YGC(Young GC 次数) 视吞吐而定 每秒几十次 新生代太小或对象分配过快

3.2 jmap -histo 快速定位嫌疑类

# 按实例数降序排列,看前 30 个类
jmap -histo <java_pid> | head -30

输出:

 num     #instances         #bytes  class name
   1:       1827364      438567360  [C            ← char[](通常是 String)
   2:       1523456      365629440  java.lang.String
   3:        234567       56194080  java.util.HashMap$Node
   4:        123456       29629440  com.example.dto.OrderDTO   ← ⚠️ 你的 DTO
   5:         98765       23703600  java.util.ArrayList

分析思路:

  • char[]String 排前两名正常 —— 任何 Java 应用都这样
  • 你的业务类(DTO、Entity)突然排进前 10 —— 大概率是内存泄漏点
  • HashMap$Node 异常多 —— 某个 Map 在不断增长却没清理

3.3 jmap -dump 生成 heap dump

# 生成 heap dump 文件
jmap -dump:format=b,file=/tmp/heap_$(date +%Y%m%d_%H%M%S).hprof <java_pid>

⚠️ dump 期间 JVM 会暂停(STW),线上慎用。优先用 -histo 快速诊断。

3.4 让 JVM 在 OOM 时自动 dump

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump/heap.hprof
-XX:OnOutOfMemoryError="kill -9 %p"  # OOM 后自动重启(可选)

3.5 内存泄漏常见根因

根因 表现 jmap -histo 特征
ThreadLocal 未清理 老年代缓慢增长 业务 DTO 数量与请求量不成比例
静态集合无限追加 老年代持续增长 ArrayListHashMap 实例数少但 #bytes
数据库连接未关闭 连接池耗尽 不会 OOM,但线程会 WAITING
第三方库 bug 特定类的实例异常多 搜可疑的第三方类名

四、接口响应慢排查

4.1 看线程状态分布

# 统计各状态的线程数量
jstack <java_pid> | grep "java.lang.Thread.State" | sort | uniq -c | sort -rn

输出示例:

 127    java.lang.Thread.State: WAITING (parking)
  45    java.lang.Thread.State: TIMED_WAITING (parking)
  12    java.lang.Thread.State: BLOCKED
   8    java.lang.Thread.State: RUNNABLE
状态 多意味着什么
BLOCKED 锁竞争——有线程持有锁不放,其他线程干等
WAITING (parking) 在等外部条件——数据库连接、Redis 响应、下游 HTTP
TIMED_WAITING 超时等待——可能是连接池耗尽的前兆
RUNNABLE 少 线程都在等,不在干活——排查外部依赖

4.2 找到阻塞源头

# 看 BLOCKED 状态的线程在等哪个锁
jstack <java_pid> | grep -A 30 "BLOCKED"

关键信息:

- waiting to lock <0x00000007a1b2c3d8> (a java.util.HashMap)
  - locked <0x00000007a1b2c3d8> (a java.util.HashMap)
    at ...

持有锁的线程在干什么?如果在做 I/O,那就是锁粒度过大。

4.3 检查数据库连接池

# 看有多少线程在等 HikariCP 连接
jstack <java_pid> | grep -c "HikariPool.*getConnection"

如果这个数字接近 maximum-pool-size,连接池满了。


五、死锁排查

5.1 jstack 自动检测死锁

jstack <java_pid> | grep -A 50 "deadlock"

jstack 会自动检测死锁并在输出末尾列出。如果存在死锁,输出会明确标注:

Found one Java-level deadlock:
============================
"Thread-1":
  waiting to lock monitor 0x00007f...  (object 0x00000007a1b2c3d8, a java.lang.Object),
  which is held by "Thread-2"

"Thread-2":
  waiting to lock monitor 0x00007f...  (object 0x00000007a1b2c3e0, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================

5.2 如果没有自动检测

# 手动搜 BLOCKED 线程及其等待的锁
jstack <java_pid> | grep -B 2 -A 10 "waiting to lock"

六、线程数异常排查

6.1 查看总线程数

# 方式一
jstack <java_pid> | grep "^""" | wc -l

# 方式二:查看线程列表
jcmd <java_pid> Thread.print | grep "^""" | wc -l

6.2 按线程名分组统计

jstack <java_pid> | grep "^""" | awk -F'"' '{print $2}' | \
  sed 's/-[0-9]*$//' | sort | uniq -c | sort -rn | head -20

输出:

 200 http-nio-8080-exec
  50 SimpleAsyncTaskExecutor
  30 scheduling-
  10 HikariPool
线程名 异常数量 可能原因
http-nio-xxx-exec > 200 Tomcat 线程池配大了或请求积压
SimpleAsyncTaskExecutor 几千 @Async 没配线程池,每次新建线程
HikariPool > 连接池大小 连接池泄露
自定义线程名 持续增长 线程池没设 max-size 或线程未回收

七、JVM 参数推荐

# 生产环境基线 JVM 参数
java \
  -Xms4g -Xmx4g \                          # 堆大小,min=max 避免动态扩缩
  -XX:+UseG1GC \                            # G1 收集器(大堆首选)
  -XX:MaxGCPauseMillis=200 \                # GC 停顿目标 200ms
  -XX:+HeapDumpOnOutOfMemoryError \         # OOM 自动 dump
  -XX:HeapDumpPath=/data/dump/ \            # dump 路径
  -XX:+PrintGCDetails \                     # GC 日志
  -XX:+PrintGCDateStamps \
  -Xloggc:/data/logs/gc.log \
  -jar app.jar

八、命令速查总表

症状              第一步命令                  第二步                    第三步
────────────────────────────────────────────────────────────────────────────
CPU 飙高           top -Hp <pid>             printf "%x\n" <tid>        jstack <pid> | grep 0x<hex>
                                                                        定位到代码行

内存上涨           jstat -gc <pid> 1000 10    jmap -histo <pid>         jmap -dump:format=b,file=...
                   → 看 OU/OC 使用率          → 找嫌疑类                 → 离线分析

Full GC 频繁       jstat -gc <pid> 1000 5     jmap -histo <pid>         同上
                   → 看 FGC 次数和耗时        → 确认泄漏

接口慢             jstack <pid> | grep        jstack <pid> | grep       检查慢 SQL、连接池、
                   "Thread.State"             -A 30 "BLOCKED"            下游 HTTP 超时
                   → 看状态分布               → 分析阻塞链

死锁               jstack <pid> | grep        jstack 输出末尾自动
                   -A 50 "deadlock"           标注死锁详情

线程数异常         jstack <pid> | grep       按线程名分组统计
                   "^""" | wc -l              → 定位线程泄漏源

OOM 已发生         查看 hs_err_pid.log        jmap -dump(如仍在运行)   MAT / JProfiler
                   → JVM 崩溃日志              → 分析 dump                → 找最大对象

核心原则: 先确定症状类型,再走对应路径。不要上来就 jstack 全量 dump 然后人肉分析——"先分类,再深入"比"一把梭"快十倍。

📋 文章摘要

本文系统整理了 Java 线上问题排查的标准命令和完整路径。覆盖六大场景:CPU 飙高(top -Hp → printf 转十六进制 → jstack 定位代码行)、内存泄漏(jstat 看 GC → jmap -histo 找嫌疑类 → jmap dump 离线分析)、接口慢(线程状态分布 → 阻塞链分析 → 连接池检查)、死锁、线程数异常、OOM 应急处理。每个场景配有完整命令示例、输出解读表和决策逻辑,文末附 JVM 生产参数推荐和命令速查总表。适合需要不依赖 Arthas 等第三方工具、用 JDK 自带命令完成排查的 Java 后端开发者。

更多推荐