适合人群 :Java 微服务测试工程师、SDET、性能调优工程师
阅读时长:约 30 分钟
前言
在微服务架构中,Java 服务(Spring Boot / Spring Cloud)是最常见的服务实现 。当线上出现 CPU 飙高、内存溢出(OOM)、响应变慢、接口超时 时,只会看监控图表是远远不够的------你必须能深入 JVM 内部,拿到线程栈、堆内存快照、GC 数据这三把钥匙。
JDK 自带的命令行工具三剑客,正是解决这些问题的利器:
| 工具 | 作用域 | 解决的核心问题 |
|---|---|---|
| jstack | 线程栈(Thread Stack) | CPU 飙高、死锁、线程阻塞、响应慢 |
| jmap | 堆内存(Heap Dump) | 内存泄漏、OOM、对象膨胀 |
| jstat | GC 统计(Garbage Collection) | GC 频繁、停顿时间长、吞吐下降 |
💡 重要前提 :这三个工具都是 JDK 自带 的(
$JAVA_HOME/bin/下),无需额外安装 。只要能登录到微服务所在的机器(或容器),就能直接使用。这也是它们成为"生产环境排障首选"的原因------零侵入、随时可用。
本文按 "什么时候用 → 怎么用 → 怎么看 → 真实案例" 的逻辑组织,所有命令均在 JDK 8 / JDK 11 / JDK 17 下验证,可直接在测试/生产环境复用。
目录
- [一、前置知识:先找到目标 JVM 进程](#一、前置知识:先找到目标 JVM 进程)
- 二、jstack:线程栈分析利器
- 三、jmap:堆内存快照分析
- [四、jstat:GC 实时监控](#四、jstat:GC 实时监控)
- 五、三剑客联动:完整排查流程
- [六、容器与 Kubernetes 环境下的特殊注意事项](#六、容器与 Kubernetes 环境下的特殊注意事项)
- 七、常见故障排查实战案例
- 八、生产环境最佳实践与风险
- 附:命令速查表
一、前置知识:先找到目标 JVM 进程
三个工具的第一步都是 拿到 Java 进程的 PID。下面这些命令务必先掌握。
1.1 jps:JDK 专用进程查看器
bash
jps # 列出所有 Java 进程(PID + 主类名)
jps -l # 显示完整主类/ jar 路径
jps -v # 显示 JVM 启动参数(内存、GC 等)
jps -mlv # 全部信息
示例输出:
12345 OrderServiceApplication # Spring Boot 启动类
23456 Jps -mlv
⚠️ 注意:
jps只能看到当前用户 的 Java 进程。生产环境 Java 服务通常用专用用户(如appuser)运行,需sudo -u appuser jps。
1.2 配合 Linux 命令定位
bash
# 按服务名查 PID
ps aux | grep OrderService
pgrep -f "order-service"
# 已知端口反查进程
lsof -i:8080
ss -tlnp | grep 8080
1.3 获取 JVM 基本信息
拿到 PID 后,先用 jinfo 确认 JVM 版本和参数,避免分析时踩坑:
bash
jinfo -flags 12345 # 查看 JVM 参数(堆大小、GC 器等)
jinfo -sysprops 12345 | grep "java.version" # 查看 Java 版本
二、jstack:线程栈分析利器
核心用途 :CPU 飙高、死锁、线程长时间阻塞、接口响应变慢 。它能给你某一时刻所有线程的调用栈,就像给 JVM 拍了一张"X 光片"。
2.1 基本用法
bash
jstack 12345 # 导出线程栈(最常用)
jstack -l 12345 # 同时打印锁信息(排查死锁必加)
jstack -F 12345 # 强制 dump(进程无响应时用)
jstack -m 12345 # 同时打印 native (C/C++) 栈
💡 生产建议 :
-l排查死锁/锁竞争必带;普通 CPU 分析用默认即可。导出后立即备份,线程栈是瞬时快照,过时不候。
2.2 导出与分析(CPU 飙高标准流程)
这是最经典的实战场景,务必掌握:
bash
# 步骤1:找到 CPU 最高的进程
top -c | head -20
# 步骤2:找到该进程内 CPU 最高的线程(记录 TID,如 12367)
top -H -p 12345
# 步骤3:将 TID 转成十六进制(jstack 里线程号是十六进制)
printf "%x\n" 12367 # 输出: 304f
# 步骤4:导出线程栈
jstack 12345 > thread_dump_$(date +%F_%H%M%S).log
# 步骤5:直接定位高 CPU 线程(grep 十六进制 tid)
grep -A 30 "nid=0x304f" thread_dump.log
示例输出解读:
text
"http-nio-8080-exec-12" #42 daemon prio=5 os_prio=0 cpu=8923.45ms
java.lang.Thread.State: RUNNABLE
at com.example.service.OrderService.calculate(OrderService.java:128)
at com.example.controller.OrderController.create(OrderController.java:56)
→ 结论 :线程 42 长时间 RUNNABLE,卡在 OrderService.calculate 的 128 行------死循环或复杂计算,这就是 CPU 高的根因。
2.3 线程状态解读(必会)
jstack 中线程有 6 种状态,每种对应不同问题:
| 状态 | 含义 | 排查方向 |
|---|---|---|
| RUNNABLE | 正在运行或就绪 | CPU 高、死循环 |
| BLOCKED | 等 monitor 锁(synchronized) | 锁竞争 |
| WAITING | 无限期等待(wait/join/park) | 线程池饥饿 |
| TIMED_WAITING | 限期等待(sleep/wait with timeout) | 正常或超时配置 |
| TERMINATED | 已终止 | --- |
关键观察 :大量线程 BLOCKED 在同一锁 → 锁竞争 ;大量 WAITING 在 park → 线程池满/下游慢。
2.4 死锁排查
bash
jstack -l 12345 | grep -i "deadlock"
若检测到死锁,jstack 会直接输出死锁线程对和持锁信息,清晰指出:
text
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f... (object 0x000000076ab12345),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x00007f... (object 0x000000076ab67890),
which is held by "Thread-1"
2.5 进阶:异步线程栈(Async Profiler)
对于复杂场景,可采样一段时间内的线程栈分布:
bash
# 采样 30 秒,生成火焰图(需单独下载 async-profiler)
./profiler.sh -d 30 -e cpu -f flamegraph.html 12345
火焰图能直观看出 CPU 时间都耗在哪个方法上。
三、jmap:堆内存快照分析
核心用途 :内存泄漏、OutOfMemoryError、对象膨胀、堆外内存异常 。它用来生成 heap dump(堆转储文件),再用 MAT / JProfiler 分析。
3.1 基本用法
bash
jmap -heap 12345 # 堆内存概览(快速诊断)
jmap -histo 12345 | head -30 # 对象实例排行(内存泄漏快速定位)
jmap -histo:live 12345 | head -30 # 只统计存活对象(触发 Full GC)
jmap -dump:format=b,file=heap.hprof 12345 # 生成完整堆快照
jmap -dump:live,format=b,file=heap_live.hprof 12345 # 只存活对象
⚠️ 风险警告 :
jmap -dump会 STW(Stop-The-World) ,大堆(>4G)可能停顿几十秒甚至分钟级,生产慎用 !优先考虑下文-XX:+HeapDumpOnOutOfMemoryError自动 dump。
3.2 快速诊断:对象排行
无需 dump 文件,直接看哪类对象占内存最多:
bash
jmap -histo 12345 | head -20
示例输出:
text
num #instances #bytes class name
----------------------------------------------
1: 2580343 123456789 [B (byte[])
2: 1820456 87345612 java.lang.String
3: 456789 34567890 com.example.Order
4: 12345 2345678 [Ljava.util.HashMap$Node;
→ 结论 :Order 对象 45 万个实例异常偏多 → 疑似缓存未清理或集合泄漏。
3.3 生成堆快照(Heap Dump)
bash
# 生成快照(生产建议在低峰期)
jmap -dump:format=b,file=/tmp/heap_$(date +%F_%H%M%S).hprof 12345
# 压缩传输到本地分析
gzip /tmp/heap_*.hprof
scp server:/tmp/heap_*.hprof.gz .
用 MAT(Eclipse Memory Analyzer)打开 .hprof 文件,重点关注:
- Dominator Tree:哪些对象占用内存最大
- Path to GC Roots:泄漏对象为什么没被回收
- Leak Suspects Report:MAT 自动给出的泄漏嫌疑点
3.4 自动 dump(生产推荐配置)
在 JVM 启动参数中加入,OOM 时自动生成快照,避免事后难复现:
bash
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump/
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
3.5 堆外内存(Direct Memory / Metaspace)
Java 微服务常因 NIO、Netty 导致堆外内存泄漏,jmap 看不到,需用:
bash
# 查看 Metaspace / 直接内存
jstat -gc 12345 | awk '{print "Metaspace:", $9}'
# 查看 Native Memory(JDK 11+)
jcmd 12345 VM.native_memory
四、jstat:GC 实时监控
核心用途 :GC 频繁、停顿时间长、吞吐下降、晋升失败 。它持续采样 GC 数据 ,无需 dump,对生产几乎无影响 ,是最安全的日常监控工具。
4.1 基本用法
bash
jstat -gc 12345 1s # 每 1 秒采集一次 GC 数据(最常用)
jstat -gcutil 12345 1s # 百分比形式(更直观)
jstat -gccause 12345 1s # 额外显示 GC 原因
jstat -gccapacity 12345 # 各代容量
jstat -gcmetacapacity 12345 # Metaspace
jstat -gcnew 12345 1s # 新生代
jstat -gcold 12345 1s # 老年代
4.2 -gcutil 输出解读(重点)
bash
jstat -gcutil 12345 1s
示例输出:
text
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 98.34 76.54 85.67 92.34 90.12 1523 45.678 23 12.345 58.023
| 列 | 含义 | 告警阈值 |
|---|---|---|
| S0 / S1 | Survivor 0/1 使用率 | 频繁 0↔100 切换 |
| E | Eden 区使用率 | 快速涨到 100% → YGC 频繁 |
| O | 老年代使用率 | 持续 > 90% → 内存泄漏嫌疑 |
| M | Metaspace 使用率 | 接近上限 → 类加载泄漏 |
| YGC / YGCT | 年轻代 GC 次数 / 总时间 | YGCT 增长快 → 对象创建过快 |
| FGC / FGCT | Full GC 次数 / 总时间 | FGC 频繁或 FGCT 大 → 严重问题 |
4.3 典型异常模式识别
① 内存泄漏(最典型)
O 列:45% → 60% → 78% → 89% → 95%(持续上涨不回落)
FGC 列:3 → 8 → 15 → 27(快速增长)
→ Eden 每次 GC 后对象大量晋升,老年代只涨不跌 → 内存泄漏铁证。
② GC 频繁导致停顿
YGC 一秒增加好几,YGCT 累计快速上升
→ 对象创建速率过高(如循环里 new 大对象)→ 优化代码或加大新生代。
③ Full GC 频繁
FGC 持续增加,FGCT 单次 > 1s
→ 老年代不够或碎片化 → 调大 -Xmx 或换 G1/ZGC。
4.4 结合监控做容量规划
bash
# 记录一天 GC 数据,用于容量分析
jstat -gcutil 12345 60 > /tmp/gc_$(date +%F).log &
用采集数据分析 GC 频率、晋升速率、停顿占比 ,为 堆内存调优 提供依据。
五、三剑客联动:完整排查流程
这才是高级工程师的核心能力 ------把三个工具串联起来,形成标准排查闭环。
5.1 CPU 飙高排查流程(jstack 为主)
1. top -c → 找到高 CPU 的 Java 进程 PID
2. top -H -p <PID> → 找到高 CPU 线程 TID
3. printf "%x\n" <TID> → 转十六进制 nid
4. jstack <PID> > dump.log → 导出线程栈
5. grep -A 30 "nid=0x<nid>" dump.log → 定位到具体方法
6. 结合代码分析(死循环/正则回溯/序列化) → 修复
5.2 内存泄漏排查流程(jmap + jstat)
1. jstat -gcutil <PID> 1s → 观察 O 列是否只涨不跌
2. jmap -histo <PID> | head -20 → 快速看异常对象
3. jmap -dump:live,format=b,file=heap.hprof <PID> → 生成快照
4. MAT 打开分析 Dominator Tree / GC Roots → 定位泄漏点
5. 修复代码(关闭资源/清理缓存/弱引用) → 验证
5.3 响应变慢综合排查(三剑客协作)
现象:接口 P99 从 200ms 涨到 2s
① jstat -gcutil <PID> 1s
→ FGC 频繁?是 → GC 停顿导致(调堆/换 GC 器)
→ 否 → 进入 ②
② jstack -l <PID> > dump.log
→ 大量线程 BLOCKED?是 → 锁竞争(优化锁粒度)
→ 大量 WAITING (park)?是 → 线程池满/下游慢
→ 少量 RUNNABLE 占 CPU?是 → 热点方法(优化算法)
③ jmap -histo <PID>
→ 某类对象异常多?是 → 内存膨胀(分页/缓存限制)
六、容器与 Kubernetes 环境下的特殊注意事项
微服务常跑在 Docker/K8s 里,直接用 jstack/jmap 会踩坑。务必看完本节。
6.1 容器内的 JDK 工具
bash
# 进入容器
docker exec -it <container> /bin/bash
# 确认 JDK 工具可用(基础镜像可能只有 JRE,缺 jstack/jmap!)
which jstack jmap jstat
# 若没有,需切换到 JDK 基础镜像,或挂载 JDK 工具
⚠️ 常见坑 :
openjdk:17-jre-slim镜像不含 jstack/jmap !生产建议使用openjdk:17-jdk或单独挂载jstack二进制。
6.2 K8s 中常见问题:PID 1 与权限
bash
# 多容器 Pod 需指定 container
kubectl exec -it <pod> -c <container> -- /bin/bash
# jstack 需要同用户(容器里通常是 root,问题不大)
# 若报 "Unable to attach to process",检查:
# 1. /proc/sys/kernel/yama/ptrace_scope
# 2. 容器是否加了 --cap-add=SYS_PTRACE
Deployment 需添加能力:
yaml
securityContext:
capabilities:
add: ["SYS_PTRACE"]
6.3 K8s 中采集并导出 dump 文件
bash
# 容器内生成 dump
kubectl exec <pod> -- jmap -dump:format=b,file=/tmp/heap.hprof <PID>
# 拷贝到本地
kubectl cp <namespace>/<pod>:/tmp/heap.hprof ./heap.hprof
# 或直接用 kubectl 端口转发 + jconsole 远程连接
kubectl port-forward <pod> 9010:9010
6.4 现代替代方案(JDK 9+)
JDK 9 后推荐使用统一工具 jcmd,功能覆盖三剑客且更安全:
bash
jcmd 12345 Thread.print # 等价于 jstack
jcmd 12345 GC.heap_dump /tmp/heap.hprof # 等价于 jmap -dump
jcmd 12345 GC.class_histogram | head -30 # 等价于 jmap -histo
jcmd 12345 PerfCounter.print # JVM 内部性能计数
💡 K8s 环境下优先用
jcmd,兼容性好、参数简单。
七、常见故障排查实战案例
案例 1:CPU 突然 100%
现象:某订单服务 CPU 打满,响应超时。
排查:
bash
top -H -p 12345 # TID=12401 占 CPU 99%
printf "%x\n" 12401 # 0x3071
jstack 12345 | grep -A 40 "nid=0x3071"
发现 :线程卡在 String.matches() 调用正则------正则灾难性回溯。
修复 :改用预编译 Pattern.compile() 并限制输入长度。
案例 2:频繁 Full GC,老年代只涨不跌
现象:服务运行一天后响应变慢,FGC 每分钟一次。
排查:
bash
jstat -gcutil 12345 1s # O 列 90%→95%,FGC 飙升
jmap -histo:live 12345 | head -10 # HashMap$Node 和自定义 CacheItem 极多
发现 :本地缓存 ConcurrentHashMap 无过期淘汰策略,条目无限增长。
修复 :改用 Caffeine 缓存,设置 maximumSize 和 expireAfterWrite。
案例 3:接口偶发超时,无明显规律
排查:
bash
jstack -l 12345 > dump.log
# 分析线程状态分布
grep "java.lang.Thread.State" dump.log | sort | uniq -c
发现 :大量 http-nio 线程 BLOCKED 在同一把 synchronized 锁上。
修复 :锁粒度从方法级改为细粒度,热点代码改用 ConcurrentHashMap。
案例 4:Metaspace OOM
现象 :java.lang.OutOfMemoryError: Metaspace。
排查:
bash
jstat -gcmetacapacity 12345 # Metaspace 已满
jmap -clstats 12345 # 类加载器统计
发现:Groovy 动态脚本每次执行都生成新类,未复用。
修复 :脚本编译结果缓存复用,或增大 -XX:MaxMetaspaceSize。
八、生产环境最佳实践与风险
8.1 安全使用清单
| 工具 | 生产风险 | 建议 |
|---|---|---|
| jstack | 几乎无风险 | 可放心使用,-F 谨慎 |
| jstat | 无风险 | 适合长期监控 |
| jmap -dump | STW 停顿 | 大堆慎用,优先自动 dump |
| jmap -histo:live | 触发 Full GC | 生产标注,避免高峰期 |
8.2 推荐 JVM 参数(开箱即用)
bash
# 基础调优 + 自动诊断
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump/
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/var/log/gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=100M
8.3 自动化脚本:一键采集三剑客数据
将排查流程脚本化,故障时 30 秒拿到全部数据:
bash
#!/bin/bash
# save as: jvm_dump.sh
PID=$1
OUTDIR="/tmp/jvm_dump_$(date +%F_%H%M%S)"
mkdir -p $OUTDIR
echo "=== Collecting jstack ==="
jstack -l $PID > $OUTDIR/jstack.log
echo "=== Collecting jmap histo ==="
jmap -histo:live $PID > $OUTDIR/histo.log 2>&1
echo "=== Collecting jstat (30s) ==="
jstat -gcutil $PID 1 30 > $OUTDIR/jstat.log 2>&1
echo "=== Done: $OUTDIR ==="
ls -lh $OUTDIR
使用:
bash
chmod +x jvm_dump.sh
./jvm_dump.sh 12345
附:命令速查表
jstack
| 命令 | 用途 |
|---|---|
jstack <PID> |
导出线程栈 |
jstack -l <PID> |
含锁信息(死锁排查) |
jstack -F <PID> |
强制 dump |
grep -A 30 "nid=0x304f" dump.log |
定位指定线程 |
jmap
| 命令 | 用途 |
|---|---|
jmap -heap <PID> |
堆概览 |
jmap -histo <PID> |
对象排行 |
jmap -histo:live <PID> |
存活对象(触发 FGC) |
jmap -dump:format=b,file=heap.hprof <PID> |
生成堆快照 |
jstat
| 命令 | 用途 |
|---|---|
jstat -gc <PID> 1s |
GC 明细 |
jstat -gcutil <PID> 1s |
GC 百分比(推荐) |
jstat -gccause <PID> 1s |
含 GC 原因 |
jstat -gcmetacapacity <PID> |
Metaspace |
前置工具
| 命令 | 用途 |
|---|---|
jps -lv |
查 Java 进程 |
jinfo -flags <PID> |
JVM 参数 |
jcmd <PID> <cmd> |
统一诊断(JDK 9+) |
top -H -p <PID> |
进程内线程 CPU |
结语
jstack、jmap、jstat 是 Java 微服务性能调优的 "听诊器、CT、血常规" ------一个看线程、一个看内存、一个看 GC,配合使用才能形成完整诊断。
核心记忆点:
- 🔥 CPU 高 → jstack(线程栈 + top -H)
- 💧 内存涨 → jstat + jmap(GC 趋势 + 堆快照)
- ⚡ 日常监控 → jstat(无侵入最安全)
- 🐳 容器环境 → 注意 JDK 镜像 + ptrace 权限
当你能在 5 分钟内用三剑客定位到具体代码行 ,你就已经具备 高级性能调优工程师 的能力了。
本文命令在 JDK 8 / 11 / 17 + CentOS 7 / Ubuntu 20.04 下验证通过。生产环境执行 jmap -dump 前请务必评估 STW 风险。