线上排障方法论
线上排障方法论
为什么需要掌握 JVM 诊断工具
在生产环境中,Java 应用可能遇到各种棘手的问题:
常见线上问题类型
┌─────────────────────────────────────────────────────────────┐
│ 常见 JVM 线上问题 │
├─────────────────────────────────────────────────────────────┤
│ 1. CPU 飙高 │
│ - 死循环、自旋等待 │
│ - 频繁 GC │
│ - 密集计算任务 │
├─────────────────────────────────────────────────────────────┤
│ 2. 内存问题 │
│ - 内存泄漏(Memory Leak) │
│ - 内存溢出(OOM) │
│ - 直接内存溢出 │
│ - Metaspace 溢出 │
├─────────────────────────────────────────────────────────────┤
│ 3. GC 问题 │
│ - 频繁 Full GC │
│ - GC 停顿时间过长 │
│ - Young GC 过于频繁 │
├─────────────────────────────────────────────────────────────┤
│ 4. 线程问题 │
│ - 线程死锁 │
│ - 线程阻塞 │
│ - 线程泄漏(线程数持续增长) │
│ - 线程池耗尽 │
├─────────────────────────────────────────────────────────────┤
│ 5. 类加载问题 │
│ - ClassNotFoundException │
│ - ClassCastException │
│ - 类冲突(jar 包冲突) │
└─────────────────────────────────────────────────────────────┘排障的核心原则
正确顺序:现象 → 取证 → 根因
- 先看监控和现象,确定问题类型(CPU/内存/GC/线程)
- 再选合适工具取证,收集线程栈、堆快照、GC 日志等
- 最后回到代码定位根因,找到具体代码位置并修复
错误做法:
- × 一出问题就抓 heap dump(可能不是内存问题)
- × 还没确认问题类型就改 JVM 参数
- × 不看监控直接分析工具输出
- × 重启后现场丢失,无法定位根因
一、JDK 自带命令行工具
JDK 自带了一系列强大的命令行诊断工具,是排查线上问题的基础武器。这些工具位于 JDK 的 bin 目录下,可以直接使用。
1.1 工具选择速查表
┌────────────────────────────────────────────────────────────────────────────┐
│ 命令行诊断工具速查 │
├───────────┬──────────────────────┬─────────────────────────────────────────┤
│ 工具 │ 主要用途 │ 常用场景 │
├───────────┼──────────────────────┼─────────────────────────────────────────┤
│ jps │ 查看 JVM 进程 │ 找到目标进程 ID │
├───────────┼──────────────────────┼─────────────────────────────────────────┤
│ jstat │ 监控 GC 和内存 │ 实时监控 GC 频率、内存使用 │
├───────────┼──────────────────────┼─────────────────────────────────────────┤
│ jinfo │ 查看和修改 JVM 参数 │ 查看参数配置、动态开启调试参数 │
├───────────┼──────────────────────┼─────────────────────────────────────────┤
│ jmap │ 堆内存分析 │ 生成堆转储、查看对象统计 │
├───────────┼──────────────────────┼─────────────────────────────────────────┤
│ jstack │ 线程栈分析 │ CPU 高、死锁、线程阻塞 │
├───────────┼──────────────────────┼─────────────────────────────────────────┤
│ jcmd │ 多功能诊断入口 │ 推荐使用,整合多种功能 │
└───────────┴──────────────────────┴─────────────────────────────────────────┘1.2 jps:JVM 进程查看工具
作用: 列出正在运行的 JVM 进程,类似于 Linux 的 ps 命令,但只显示 Java 进程。
基本用法
## 列出 JVM 进程(只显示主类名)
jps
## 输出示例:
## 12345 Bootstrap
## 12346 Jps
## 12347 org.gradle.launcher.daemon.bootstrap.GradleDaemon
## 显示完整主类名或 JAR 路径
jps -l
## 输出示例:
## 12345 /opt/tomcat/bin/bootstrap.jar
## 12346 sun.tools.jps.Jps
## 12347 org.gradle.launcher.daemon.bootstrap.GradleDaemon
## 显示 JVM 参数
jps -v
## 输出示例:
## 12345 Bootstrap -Xms2g -Xmx2g -XX:+UseG1GC -Djava.awt.headless=true
## 12346 Jps -Dapplication.home=/usr/lib/jvm/java-11-openjdk
## 显示主函数参数
jps -m
## 输出示例:
## 12345 Bootstrap start
## 12346 Jps -m常用选项对比
| 选项 | 说明 | 示例输出 |
|---|---|---|
| 无选项 | 只显示进程 ID 和主类简称 | 12345 Bootstrap |
| -l | 显示完整主类名或 JAR 路径 | 12345 /opt/tomcat/bin/bootstrap.jar |
| -v | 显示 JVM 启动参数 | 12345 Bootstrap -Xms2g -Xmx2g |
| -m | 显示主函数参数 | 12345 Bootstrap start |
| -q | 只显示进程 ID | 12345 |
实战技巧
技巧 1:批量监控所有 JVM 进程的 GC 状态
## 一键查看所有 JVM 进程的 GC 状态
for pid in $(jps -q | grep -v Jps); do
echo "=== PID: $pid ==="
jstat -gcutil $pid 2>/dev/null || echo "无法连接进程"
done技巧 2:查找特定应用的进程
## 查找包含 "myapp" 的进程
jps -l | grep myapp
## 输出示例:
## 12345 com.example.myapp.Application
## 提取进程 ID
jps -l | grep myapp | awk '{print $1}'技巧 3:远程监控(需要配置 RMI)
## 远程查看 JVM 进程(需要在远程主机配置 JMX)
jps -l remotehost:port- jps 只能查看当前用户的 Java 进程
- 如果 jps 无输出,可能是因为进程已经退出或权限不足
- jps 通过 Java 的 Attach API 获取进程信息,如果 Attach API 被禁用则无法使用
1.3 jstat:JVM 统计监控工具
作用: 监控 JVM 类加载、内存、垃圾收集、JIT 编译等运行数据。jstat 是排查 GC 问题的首选工具。
核心选项
## GC 统计(最常用)
jstat -gc <pid>
## GC 汇总统计(百分比形式,更直观)
jstat -gcutil <pid>
## GC 原因统计
jstat -gccause <pid>
## 类加载统计
jstat -class <pid>
## JIT 编译统计
jstat -compiler <pid>
## 打印 GC 统计摘要
jstat -gcprint <pid>jstat -gc 输出详解
jstat -gc 12345
## 输出示例:
## S0C S1C S0U S1U EC EU OC OU MC MU YGC YGCT FGC FGCT GCT
## 10240 10240 0.0 8192.0 81920 40960.0 163840 131072.0 51200 48000 15 0.156 3 0.234 0.390参数说明:
新生代(Young Generation)
├── S0C: Survivor 0 区容量(KB)
├── S1C: Survivor 1 区容量(KB)
├── S0U: Survivor 0 区已用(KB)
├── S1U: Survivor 1 区已用(KB)
├── EC : Eden 区容量(KB)
└── EU : Eden 区已用(KB)
老年代(Old Generation)
├── OC : 老年代容量(KB)
└── OU : 老年代已用(KB)
元空间(Metaspace)
├── MC : 元空间容量(KB)
├── MU : 元空间已用(KB)
├── CCSC: 压缩类空间容量(KB)
└── CCSU: 压缩类空间已用(KB)
GC 统计
├── YGC : Young GC 次数
├── YGCT: Young GC 总时间(秒)
├── FGC : Full GC 次数
├── FGCT: Full GC 总时间(秒)
├── GCT : GC 总时间(秒)
└── LGCC: 上次 GC 原因(jstat -gccause 显示)jstat -gcutil 实战监控
## 每隔 1 秒输出一次,共输出 10 次
jstat -gcutil 12345 1000 10
## 输出示例:
## S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
## 0.00 93.47 24.62 46.68 93.47 88.71 15 0.156 3 0.234 0.390
## 0.00 93.47 28.45 46.68 93.47 88.71 15 0.156 3 0.234 0.390
## 0.00 93.47 45.12 46.68 93.47 88.71 15 0.156 3 0.234 0.390
## 85.23 0.00 0.00 47.25 93.47 88.71 16 0.168 3 0.234 0.402
## 85.23 0.00 15.34 47.25 93.47 88.71 16 0.168 3 0.234 0.402
## 持续监控(不指定次数则一直输出)
jstat -gcutil 12345 1000监控指标解读
正常模式:
特征:
- Eden 区使用率周期性增长(0% → 100% → 0%)
- Survivor 区使用率稳定
- 老年代使用率稳定或缓慢增长
- YGC 次数稳定增长,FGC 很少
判断标准:
- Young GC 频率:正常范围 1-10 次/秒
- Full GC 频率:正常范围 0-1 次/小时
- GC 时间占比:正常 < 5%异常模式 1:老年代持续增长
问题:老年代持续增长,即将触发 Full GC
现象:
- O 列持续增长,接近 100%
- FGC 次数开始增加
- FGCT 时间增长
可能原因:
- 内存泄漏
- 大对象过多
- 缓存未清理
- 对象晋升过快
解决方案:
- 增大堆内存
- 优化代码减少对象创建
- 使用有界缓存
- 分析 heap dump 找出大对象异常模式 2:频繁 Young GC
问题:每秒 10+ 次 Young GC
现象:
- E 列快速从 0 增长到 100
- YGC 次数快速增长
- YGCT 时间累积
可能原因:
- 新生代太小
- 对象创建过快
- Eden 区设置过小
解决方案:
- 增大新生代(-Xmn 或 -XX:NewRatio)
- 优化代码减少对象创建
- 使用对象池重用对象异常模式 3:频繁 Full GC
问题:频繁 Full GC,系统停顿
现象:
- FGC 次数频繁增长
- FGCT 时间很长
- 系统卡顿
可能原因:
- 内存不足
- 内存泄漏
- 老年代空间不足
- Metaspace 空间不足
解决方案:
- 增大堆内存
- 排查内存泄漏
- 增大 Metaspace(-XX:MaxMetaspaceSize)
- 调整 GC 策略jstat 实战案例
案例 1:监控 GC 趋势
## 每秒输出一次,监控 1 分钟
jstat -gcutil 12345 1000 60 > gc_monitor.log
## 分析趋势
## 查看 YGC 频率
grep -o "YGC.*" gc_monitor.log | awk '{print $1, $2}' | tail -10
## 查看老年代增长趋势
grep -o "O.*" gc_monitor.log | awk '{print $1}' | tail -20案例 2:检测内存泄漏
## 每隔 10 秒输出一次,持续监控
jstat -gcutil 12345 10000 | tee memory_leak_check.log
## 观察老年代(第 4 列)是否持续增长
## 正常:老年代使用率在 GC 后会下降或保持稳定
## 异常:老年代使用率持续上升,GC 后不下降1.4 jinfo:JVM 配置信息工具
作用: 实时查看和调整 JVM 配置参数。jinfo 可以查看所有 JVM 参数,也可以动态修改部分参数。
基本用法
## 查看所有 JVM 参数
jinfo <pid>
## 输出示例:
## Java System Properties:
## #Mon Mar 30 00:00:00 CST 2026
## java.runtime.name=OpenJDK Runtime Environment
## java.vm.version=11.0.11+9
## ...
##
## VM Flags:
## -XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1HeapRegionSize=1048576 ...
##
## VM Arguments:
## jvm_args: -Xms2g -Xmx2g -XX:+UseG1GC
## java_command: com.example.Application查看特定参数
## 查看特定参数
jinfo -flag MaxHeapSize <pid>
## 输出: -XX:MaxHeapSize=2147483648
jinfo -flag UseG1GC <pid>
## 输出: -XX:+UseG1GC
## 查看所有 Flags
jinfo -flags <pid>
## 输出示例:
## VM Flags:
## -XX:CICompilerCount=4 -XX:ConcGCThreads=2 ...
## -XX:InitialHeapSize=2147483648 -XX:MaxHeapSize=2147483648 ...动态修改参数
## 动态开启 GC 日志
jinfo -flag +PrintGCDetails <pid>
## 动态关闭 GC 日志
jinfo -flag -PrintGCDetails <pid>
## 设置 HeapDumpOnOutOfMemoryError
jinfo -flag +HeapDumpOnOutOfMemoryError <pid>
## 设置 HeapDump 路径
jinfo -flag HeapDumpPath=/tmp/heap.hprof <pid>只有标记为 manageable 的参数才能在运行时修改。
查看可动态修改的参数:
## 查看所有 manageable 参数
java -XX:+PrintFlagsFinal -version | grep manageable
## 输出示例:
## intx CMSAbortablePrecleanWaitMillis = 100 {manageable}
## intx CMSTriggerInterval = -1 {manageable}
## bool HeapDumpOnOutOfMemoryError = false {manageable}
## ccstr HeapDumpPath = {manageable}
## uintx MaxHeapFreeRatio = 70 {manageable}
## uintx MinHeapFreeRatio = 40 {manageable}
## bool PrintGC = false {manageable}
## bool PrintGCDetails = false {manageable}
## bool PrintGCDateStamps = false {manageable}常见可动态修改的参数:
- PrintGCDetails / PrintGC
- HeapDumpOnOutOfMemoryError / HeapDumpPath
- MaxHeapFreeRatio / MinHeapFreeRatio
实战技巧
技巧 1:快速查看堆内存配置
## 查看堆内存相关参数
jinfo <pid> | grep -i heap
## 输出示例:
## MaxHeapSize = 2147483648 (2048.0MB)
## NewSize = 681573760 (650.0MB)
## MaxNewSize = 681573760 (650.0MB)技巧 2:临时开启 GC 日志排查问题
## 生产环境遇到 GC 问题,临时开启 GC 日志
jinfo -flag +PrintGCDetails <pid>
jinfo -flag +PrintGCDateStamps <pid>
## 观察 GC 日志输出(控制台或日志文件)
## ...
## 排查完毕后关闭
jinfo -flag -PrintGCDetails <pid>
jinfo -flag -PrintGCDateStamps <pid>1.5 jmap:内存映射工具
作用: 生成堆转储快照(heap dump)、查询堆内存详细信息、对象统计。jmap 是排查内存问题的核心工具。
生成堆转储
## 方式 1:使用 jmap 生成堆转储
jmap -dump:format=b,file=heap.hprof <pid>
## 输出示例:
## Dumping heap to /tmp/heap.hprof ...
## Heap dump file created
## 方式 2:使用 jcmd 生成堆转储(推荐)
jcmd <pid> GC.heap_dump /tmp/heap.hprof
## 生成堆转储时只包含存活对象(先执行 Full GC)
jmap -dump:live,format=b,file=heap_live.hprof <pid>
## 方式 3:使用 Arthas 生成堆转储
## heapdump /tmp/heap.hprof生成堆转储时会暂停应用(STW,Stop-The-World),大堆可能需要几分钟。
建议:
- 确认是内存问题再抓 heap dump
- 确认磁盘空间充足(堆转储文件大小 ≈ 堆大小)
- 在业务低峰期抓取
- 使用 live 选项可以减小文件大小(会触发 Full GC)
- 最好提前配置 HeapDumpOnOutOfMemoryError,OOM 时自动生成
配置 OOM 时自动生成 heap dump:
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/heapdump.hprof -jar app.jar查看堆内存使用
## 查看堆内存详细信息
jcmd <pid> GC.heap_info # JDK 9+ 推荐
# 旧版(JDK 8):jmap -heap <pid>(JDK 9+ 已移除 -heap 选项)
## 输出示例(g1gc):
## garbage-first heap total 2048M, used 512M
## region size 4M, 495 young (1980M), 5 survivors (20M)
## Metaspace used 45M, capacity 48M对象统计
## 查看堆中对象统计(对象数量、大小)
jmap -histo <pid> | head -25
## 输出示例:
## num #instances #bytes class name
## ----------------------------------------------
## 1: 12345 98765432 [B
## 2: 23456 76543210 [Ljava.lang.Object;
## 3: 34567 54321098 java.lang.String
## 4: 12345 43210987 java.util.HashMap$Node
## 5: 23456 32109876 java.lang.Integer
## 6: 34567 21098765 [C
## 7: 12345 10987654 java.util.concurrent.ConcurrentHashMap$Node
## 只统计存活对象(会触发 Full GC)
jmap -histo:live <pid> | head -25
## 输出到文件
jmap -histo <pid> > histo.txt对象统计解读:
class name 说明:
[B : byte 数组
[C : char 数组
[I : int 数组
[J : long 数组
[L<class> : 对象数组,如 [Ljava.lang.Object;
<class> : 普通对象,如 java.lang.String
分析技巧:
1. 关注 instances 数量异常大的对象
2. 关注 bytes 占用最大的对象
3. 关注自定义类是否对象数量过多
4. 对比多次 histo 结果,找出增长的对象实战案例:排查内存泄漏
步骤 1:查看对象统计
## 查看对象统计
jmap -histo:live 12345 | head -30
## 输出示例:
## num #instances #bytes class name
## ----------------------------------------------
## 1: 500000 120000000 com.example.model.UserSession
## 2: 500000 40000000 java.lang.String
## 3: 10000 20000000 [B步骤 2:生成堆转储
## 生成堆转储
jmap -dump:live,format=b,file=leak.hprof 12345步骤 3:使用 MAT 分析
详见后续章节 "MAT 分析技巧"。
1.6 jstack:线程栈跟踪工具
作用: 生成线程快照(thread dump),定位线程停顿、死锁、阻塞等问题。jstack 是排查 CPU 高和线程问题的首选工具。
基本使用
## 生成线程快照
jstack <pid>
## 保存到文件
jstack <pid> > thread_dump.txt
## 生成更详细的线程栈(包含锁信息)
jstack -l <pid> > thread_dump_detail.txt
## 强制生成线程栈(当进程无响应时)
jstack -F <pid> > thread_dump_force.txt线程状态详解
┌──────────────────────────────────────────────────────────────────────┐
│ Java 线程状态 │
├──────────────────────────────────────────────────────────────────────┤
│ 1. RUNNABLE(可运行) │
│ 线程正在运行或等待 CPU 时间片 │
│ 常见场景:执行代码、网络 I/O、文件 I/O │
│ │
│ 2. BLOCKED(阻塞) │
│ 线程等待获取监视器锁(synchronized) │
│ - 等待进入 synchronized 块 │
│ - 等待获取对象锁 │
│ │
│ 3. WAITING(无限等待) │
│ 线程等待其他线程唤醒,不会自动唤醒 │
│ - Object.wait() │
│ - Thread.join() │
│ - LockSupport.park() │
│ │
│ 4. TIMED_WAITING(计时等待) │
│ 线程等待指定时间后自动唤醒 │
│ - Thread.sleep() │
│ - Object.wait(long) │
│ - Thread.join(long) │
│ - LockSupport.parkNanos() │
│ - LockSupport.parkUntil() │
│ │
│ 5. NEW(新建) │
│ 线程已创建但未启动 │
│ │
│ 6. TERMINATED(终止) │
│ 线程已执行完毕 │
└──────────────────────────────────────────────────────────────────────┘线程状态示例
示例 1:RUNNABLE 状态(正常)
"http-nio-8080-exec-1" #10 daemon prio=5 os_prio=0 tid=0x00007f8c0c0a4800 nid=0x1234 runnable [0x00007f8bfe7f6000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:29)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:386)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:447)
at java.lang.Thread.run(Thread.java:748)
解读:线程正在执行网络读取操作,状态为 RUNNABLE,属于正常状态。示例 2:BLOCKED 状态(锁竞争)
"thread-1" #15 prio=5 os_prio=0 tid=0x00007f8c0c0b5000 nid=0x1235 waiting for monitor entry [0x00007f8bfe5f4000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.Demo.method(Demo.java:25)
- waiting to lock <0x00000000c0001234> (a java.lang.Object)
- locked <0x00000000c0005678> (a java.lang.Object)
at com.example.Demo.run(Demo.java:10)
解读:线程等待获取锁 <0x00000000c0001234>,当前被其他线程持有。
线程已经持有锁 <0x00000000c0005678>。
可能存在锁竞争或死锁。示例 3:WAITING 状态(等待唤醒)
"thread-2" #16 prio=5 os_prio=0 tid=0x00007f8c0c0b6000 nid=0x1236 in Object.wait() [0x00007f8bfe4f2000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000c0001234> (a java.lang.Object)
at java.lang.Object.wait(Object.java:502)
at com.example.Demo.consume(Demo.java:30)
at com.example.Demo.run(Demo.java:15)
解读:线程调用了 Object.wait(),等待其他线程 notify/notifyAll。
典型的生产者-消费者模式。示例 4:TIMED_WAITING 状态(睡眠)
"thread-3" #17 prio=5 os_prio=0 tid=0x00007f8c0c0b7000 nid=0x1237 waiting on condition [0x00007f8bfe3f0000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.example.Demo.run(Demo.java:20)
解读:线程调用了 Thread.sleep(),将在指定时间后自动唤醒。检测死锁
## jstack 会自动检测并打印死锁信息
jstack <pid> | grep -A 10 "Found one Java-level deadlock"
## 输出示例:
## Found one Java-level deadlock:
## =============================
## "thread-2":
## waiting to lock monitor 0x00007f8c0c00a800 (object 0x00000000c0001234),
## which is held by "thread-1"
## "thread-1":
## waiting to lock monitor 0x00007f8c0c00b800 (object 0x00000000c0005678),
## which is held by "thread-2"
##
## Java stack information for the threads listed above:
## ===================================================
## "thread-2":
## at com.example.DeadlockDemo.method2(DeadlockDemo.java:30)
## - waiting to lock <0x00000000c0001234> (a java.lang.Object)
## - locked <0x00000000c0005678> (a java.lang.Object)
## at com.example.DeadlockDemo.run(DeadlockDemo.java:15)
## "thread-1":
## at com.example.DeadlockDemo.method1(DeadlockDemo.java:20)
## - waiting to lock <0x00000000c0005678> (a java.lang.Object)
## - locked <0x00000000c0001234> (a java.lang.Object)
## at com.example.DeadlockDemo.run(DeadlockDemo.java:10)
##
## Found 1 deadlock.分析线程栈技巧
技巧 1:统计线程状态
## 统计各种状态的线程数量
jstack <pid> | grep "java.lang.Thread.State" | sort | uniq -c
## 输出示例:
## 5 java.lang.Thread.State: BLOCKED
## 10 java.lang.Thread.State: RUNNABLE
## 15 java.lang.Thread.State: TIMED_WAITING (sleeping)
## 20 java.lang.Thread.State: WAITING (on object monitor)技巧 2:查找 CPU 高的线程
## 1. 找到高 CPU 进程
top
## 输出示例:
## PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
## 12345 appuser 20 0 4.0g 1.2g 120m S 95.0 7.5 0:10.23 java
## 2. 找到高 CPU 线程
top -Hp 12345
## 输出示例:
## PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
## 12346 appuser 20 0 4.0g 1.2g 120m R 92.0 7.5 0:09.15 java
## 12347 appuser 20 0 4.0g 1.2g 120m S 1.0 7.5 0:00.10 java
## 3. 转换线程 ID 为十六进制
printf "%x\n" 12346
## 输出: 303a
## 4. 在线程栈中查找
jstack 12345 | grep -A 20 "nid=0x303a"
## 输出示例:
## "http-nio-8080-exec-10" #123 daemon prio=5 os_prio=0 tid=0x00007f8c0c0a4800 nid=0x303a runnable
## java.lang.Thread.State: RUNNABLE
## at com.example.service.CalculateService.calculate(CalculateService.java:25)
## at com.example.controller.UserController.getUser(UserController.java:50)
## ...
## 5. 定位到问题代码
## 查看 CalculateService.java 第 25 行代码技巧 3:查找 BLOCKED 线程
## 查找所有 BLOCKED 状态的线程
jstack <pid> | grep -B 1 "BLOCKED"
## 查找等待锁的线程
jstack <pid> | grep -A 5 "waiting to lock"
## 输出示例:
## at com.example.Demo.method(Demo.java:25)
## - waiting to lock <0x00000000c0001234> (a java.lang.Object)
## - locked <0x00000000c0005678> (a java.lang.Object)技巧 4:一键生成 CPU 高排查脚本
#!/bin/bash
## cpu_high_check.sh
PID=$1
if [ -z "$PID" ]; then
echo "Usage: $0 <pid>"
exit 1
fi
echo "=== 高 CPU 线程分析 ==="
## 获取 CPU 最高的 3 个线程
echo "Top 3 高 CPU 线程:"
top -Hp $PID -n 1 -b | head -n 10 | tail -n 4
## 分析每个高 CPU 线程
for TID in $(top -Hp $PID -n 1 -b | head -n 10 | tail -n 3 | awk '{print $1}'); do
HEX_TID=$(printf "%x\n" $TID)
echo ""
echo "=== 线程 $TID (0x$HEX_TID) ==="
jstack $PID | grep -A 10 "nid=0x$HEX_TID"
done
echo ""
echo "=== 线程状态统计 ==="
jstack $PID | grep "java.lang.Thread.State" | sort | uniq -c | sort -rn使用方法:
chmod +x cpu_high_check.sh
./cpu_high_check.sh 123451.7 jcmd:多功能诊断工具
作用: jcmd 是一个统一的命令入口,整合了 jstack、jmap 等多种功能。jcmd 是 JDK 推荐的多功能诊断工具。
基本使用
## 列出所有 JVM 进程
jcmd -l
## 输出示例:
## 12345 com.example.Application
## 12346 org.gradle.launcher.daemon.bootstrap.GradleDaemon
## 12347 sun.tools.jcmd.JCmd
## 查看特定进程支持的命令
jcmd <pid> help
## 输出示例:
## The following commands are available:
## VM.version
## VM.command_line
## VM.flags
## VM.system_properties
## VM.utilities
## Thread.print
## GC.heap_dump
## GC.class_histogram
## GC.run
## ...常用命令
1. 查看 JVM 信息
## 查看 JVM 版本信息
jcmd <pid> VM.version
## 输出示例:
## OpenJDK 64-Bit Server VM version 11.0.11+9
## JDK 11.0.11
## 查看 JVM 启动参数
jcmd <pid> VM.command_line
## 输出示例:
## JVM Arguments:
## jvm_args: -Xms2g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200
## java_command: com.example.Application
## java_class_path (initial): /app/classes:/app/lib/*
## 查看 JVM Flags
jcmd <pid> VM.flags
## 输出示例:
## -XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1HeapRegionSize=1048576 ...2. 线程相关
## 打印线程栈(等同于 jstack)
jcmd <pid> Thread.print
## 打印线程栈(包含锁信息)
jcmd <pid> Thread.print -l
## 输出到文件
jcmd <pid> Thread.print > thread_dump.txt3. 堆内存相关
## 生成堆转储(等同于 jmap -dump)
jcmd <pid> GC.heap_dump /tmp/heap.hprof
## 输出示例:
## Heap dump file created
## 查看类统计(等同于 jmap -histo)
jcmd <pid> GC.class_histogram
## 输出示例:
## num #instances #bytes class name
## ----------------------------------------------
## 1: 12345 98765432 [B
## 2: 23456 76543210 [Ljava.lang.Object;
## 3: 34567 54321098 java.lang.String
## 执行 GC
jcmd <pid> GC.run
## 查看 GC 堆信息
jcmd <pid> GC.heap_info4. 系统属性
## 查看系统属性
jcmd <pid> VM.system_properties
## 输出示例:
## #Mon Mar 30 00:00:00 CST 2026
## java.runtime.name=OpenJDK Runtime Environment
## java.vm.version=11.0.11+9
## java.vm.vendor=Oracle Corporation
## ...5. JVM 信息汇总
## 查看所有 JVM 信息
jcmd <pid> VM.info
## 输出示例:
## VM Arguments:
## jvm_args: -Xms2g -Xmx2g -XX:+UseG1GC
## java_command: com.example.Application
## ...jcmd vs 传统工具对比
┌────────────────────────────────────────────────────────────────────────────┐
│ jcmd vs 传统工具对比 │
├────────────────────────────────────────────────────────────────────────────┤
│ 功能 │ 传统工具 │ jcmd 命令 │
├────────────────────────────────────────────────────────────────────────────┤
│ 查看进程 │ jps │ jcmd -l │
│ 线程栈 │ jstack <pid> │ jcmd <pid> Thread.print │
│ 堆转储 │ jmap -dump │ jcmd <pid> GC.heap_dump │
│ 类统计 │ jmap -histo │ jcmd <pid> GC.class_histogram │
│ JVM 参数 │ jinfo │ jcmd <pid> VM.flags │
│ 启动参数 │ jinfo │ jcmd <pid> VM.command_line │
│ 执行 GC │ 无 │ jcmd <pid> GC.run │
└────────────────────────────────────────────────────────────────────────────┘
优势:
- jcmd 是统一入口,无需记忆多个工具
- jcmd 功能更强大,支持更多诊断命令
- jcmd 性能更好,开销更小
- jcmd 是 Oracle 推荐的诊断工具二、可视化诊断工具
2.1 JConsole:Java 监控与管理控制台
作用: 提供图形化界面监控 JVM 的内存、线程、类、CPU 等信息。JConsole 是最基础的可视化监控工具。
启动方式
## JDK 8 及以下版本自带
jconsole
## 连接方式:
## 1. 本地连接:选择本地进程,直接连接
## 2. 远程连接:需要配置 JMX远程连接配置
方式 1:启动时配置 JMX
## 启动应用时添加 JMX 参数
java \
-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9010 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false \
-Djava.rmi.server.hostname=192.168.1.100 \
-jar app.jar
## 然后在 JConsole 中连接:192.168.1.100:9010方式 2:使用认证(生产环境推荐)
## 1. 创建密码文件
cat > /opt/jmxremote.password <<EOF
monitorRole readonly123
controlRole readwrite123
EOF
## 2. 设置权限
chmod 600 /opt/jmxremote.password
## 3. 启动应用
java \
-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9010 \
-Dcom.sun.management.jmxremote.authenticate=true \
-Dcom.sun.management.jmxremote.password.file=/opt/jmxremote.password \
-Dcom.sun.management.jmxremote.ssl=false \
-Djava.rmi.server.hostname=192.168.1.100 \
-jar app.jar
## 4. 在 JConsole 中使用用户名密码连接
## 用户名: controlRole
## 密码: readwrite123主要功能
1. 概览(Overview)
显示:
- 堆内存使用趋势图
- 线程数量变化图
- 类加载数量图
- CPU 使用率图
用途:快速了解应用整体运行状态2. 内存(Memory)
功能:
- 查看各内存区域使用情况
- 手动触发 GC(点击 "Perform GC")
- 查看内存池详情
内存区域:
- Heap Memory Usage:堆内存使用
- Non-Heap Memory Usage:非堆内存使用
- Memory Pool "Eden Space":Eden 区
- Memory Pool "Survivor Space":Survivor 区
- Memory Pool "Tenured Gen":老年代
- Memory Pool "Metaspace":元空间
使用技巧:
1. 观察 "Heap Memory Usage" 图表,看内存是否持续增长
2. 点击 "Perform GC" 观察内存是否能回收
3. 如果 GC 后内存不下降,可能存在内存泄漏3. 线程(Threads)
功能:
- 查看线程数量变化
- 查看线程列表
- 检测死锁(点击 "Detect Deadlock")
- 查看线程栈
使用技巧:
1. 观察线程数量是否持续增长(线程泄漏)
2. 点击 "Detect Deadlock" 检测死锁
3. 选中线程查看线程栈,定位问题4. 类(Classes)
功能:
- 查看类加载数量
- 查看类加载/卸载统计
使用技巧:
1. 观察类加载数量是否持续增长(类泄漏)
2. 如果类加载过多,可能导致 Metaspace 溢出5. VM 概要(VM Summary)
显示:
- JVM 版本
- JVM 参数
- 系统属性
- 堆内存配置
- GC 策略
用途:快速查看 JVM 配置信息JConsole 实战案例
案例 1:监控内存泄漏
## 1. 启动 JConsole 连接应用
jconsole
## 2. 切换到 "内存" 标签
## 3. 观察 "Heap Memory Usage" 图表
## 正常模式:
## - 内存使用呈锯齿状(上升 → GC → 下降)
## - GC 后内存使用率下降明显
## 内存泄漏模式:
## - 内存使用持续上升
## - GC 后内存使用率不下降
## - 老年代使用率接近 100%
## 4. 如果发现内存泄漏,使用 jmap 生成 heap dump 分析案例 2:检测死锁
## 1. 启动 JConsole 连接应用
jconsole
## 2. 切换到 "线程" 标签
## 3. 点击 "检测死锁" 按钮
## 如果发现死锁:
## Found deadlock!
## Thread-1 waiting to lock ...
## Thread-2 waiting to lock ...
## 4. 查看死锁线程栈,定位代码位置2.2 VisualVM:多合一故障处理工具
作用: VisualVM 是最强大的 JDK 自带工具,集成了监控、分析、调优等多种功能。VisualVM 是排查问题的利器。
安装和启动
## JDK 8 及以下版本自带
jvisualvm
## JDK 9+ 需要单独下载
## https://visualvm.github.io/
## 下载后解压运行
./visualvm/bin/visualvm主要功能
1. 监控(Monitor)
图表:
- CPU 使用率
- 堆内存使用
- 类加载数量
- 线程数量
操作:
- 执行 GC:触发垃圾回收
- Heap Dump:生成堆转储
使用技巧:
1. 观察 CPU 和内存趋势
2. 点击 "Heap Dump" 生成堆转储分析2. 线程(Threads)
功能:
- 实时线程状态
- 线程时间线
- 线程 Dump
线程状态颜色:
- 绿色:Runnable
- 紫色:Sleeping
- 黄色:Wait
- 红色:Blocked
使用技巧:
1. 观察线程时间线,找出异常线程
2. 点击 "Thread Dump" 查看线程栈
3. 红色(Blocked)线程过多,说明锁竞争严重3. 抽样器(Sampler)
功能:
- CPU 抽样:分析方法执行时间
- 内存抽样:分析对象分配
操作:
1. 选择 CPU 或内存抽样
2. 点击 "Start"
3. 运行一段时间后点击 "Stop"
4. 查看抽样结果
使用技巧:
- CPU 抽样:找出耗时最长的方法
- 内存抽样:找出创建对象最多的代码4. 分析器(Profiler)
功能:
- CPU 分析:分析方法执行时间和调用次数
- 内存分析:分析对象创建和回收
注意:
- Profiler 开销较大,会影响性能
- 不适合在生产环境使用
- 建议在测试环境使用
使用技巧:
1. 选择 CPU 或内存分析
2. 点击 "Start"
3. 执行业务操作
4. 点击 "Stop" 查看结果堆转储分析
生成堆转储:
方式 1:在应用程序面板右键 → Heap Dump
方式 2:监控面板 → Heap Dump 按钮
方式 3:打开已有的 .hprof 文件堆转储分析功能:
1. 概要(Summary)
- 堆大小
- 对象数量
- 类数量
- GC Root 数量
2. 类(Classes)
- 类名
- 对象数量
- 对象大小
3. 对象(Objects)
- 对象列表
- 对象详情
- 引用关系
- 查看 GC Roots
使用技巧:
1. 切换到 "类" 标签,按大小排序
2. 找出占用内存最大的类
3. 双击类名查看对象列表
4. 右键对象 → "最近 GC Root" 查看引用链VisualVM 实战案例
案例 1:分析内存泄漏
## 1. 启动 VisualVM 连接应用
jvisualvm
## 2. 点击 "Heap Dump" 生成堆转储
## 3. 切换到 "类" 标签,按大小排序
## 输出示例:
## Class | Instances | Size
## ------------------- | --------- | ----
## byte[] | 50000 | 100 MB
## java.lang.String | 100000 | 50 MB
## UserSession | 50000 | 80 MB # 异常!
## 4. 双击 UserSession 类,查看对象列表
## 5. 右键对象 → "最近 GC Root",查看引用链
## 输出示例:
## GC Root: static field SessionManager.sessions
## └── HashMap$Node
## └── UserSession
## 6. 定位到 SessionManager.sessions 字段,发现缓存未清理案例 2:分析 CPU 高
## 1. 启动 VisualVM 连接应用
jvisualvm
## 2. 切换到 "抽样器" 标签
## 3. 选择 "CPU",点击 "Start"
## 4. 运行一段时间后点击 "Stop"
## 输出示例:
## Method | Time (ms) | Invocations
## ------------------------------- | --------- | -----------
## CalculateService.calculate() | 5000 | 100000
## UserService.getUser() | 2000 | 50000
## CacheManager.get() | 1500 | 80000
## 5. 发现 CalculateService.calculate() 耗时最长
## 6. 查看代码发现死循环或复杂计算2.3 JMC(Java Mission Control):低开销监控工具
作用: JMC 是 Oracle 官方推荐的低开销监控工具,适合生产环境使用。JMC 使用 JFR(Java Flight Recorder)技术,开销 < 1%。
安装和启动
## JDK 8 自带(商业版)
## JDK 11+ 需要单独下载
## https://www.oracle.com/java/technologies/jdk-mission-control.html
## 下载后解压运行
./jmc/jmc
## 启动应用时开启 JFR
java \
-XX:+UnlockCommercialFeatures \
-XX:+FlightRecorder \
-jar app.jarJFR(Java Flight Recorder)
特点:
优势:
- 开销极低(< 1%)
- 适合生产环境
- 记录全面(CPU、内存、GC、线程、I/O 等)
- 支持事后分析
劣势:
- JDK 8 需要商业许可
- JDK 11+ 开源免费
- 需要学习成本使用 JFR 记录
方式 1:启动时记录
## 启动时开始记录
java \
-XX:+UnlockCommercialFeatures \
-XX:+FlightRecorder \
-XX:StartFlightRecording=duration=60s,filename=myrecording.jfr \
-jar app.jar
## 记录 60 秒后保存到 myrecording.jfr方式 2:使用 jcmd 动态记录
## 启动应用(开启 JFR)
java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -jar app.jar
## 查看可用记录配置
jcmd <pid> JFR.check
## 输出示例:
## Recording 1: name=1 maxsize=100.0MB maxage=1d
## 启动记录
jcmd <pid> JFR.start name=my_recording duration=60s filename=myrecording.jfr
## 输出示例:
## Started recording "my_recording". The result will be written to:
## /path/to/myrecording.jfr
## 停止记录
jcmd <pid> JFR.stop name=my_recording
## 导出记录
jcmd <pid> JFR.dump name=my_recording filename=myrecording.jfr方式 3:使用 JMC GUI
1. 启动 JMC
2. 连接到 JVM 进程
3. 点击 "Start Flight Recording"
4. 选择记录配置和时长
5. 停止后自动打开分析界面JFR 分析
分析内容:
1. 概览(Overview)
- CPU 使用率
- 堆内存使用
- GC 统计
- 线程统计
2. 内存(Memory)
- 内存分配热点
- GC 统计
- 内存泄漏检测
3. 代码(Code)
- 方法执行时间
- 方法调用次数
- 异常统计
4. 线程(Threads)
- 线程状态
- 锁竞争
- 死锁检测
5. I/O
- 文件 I/O
- 网络 I/O
- I/O 热点
6. 系统(System)
- CPU 使用
- 系统负载
- 进程信息JMC 实战案例
案例 1:分析性能瓶颈
## 1. 启动 JFR 记录
jcmd 12345 JFR.start name=perf duration=60s filename=perf.jfr
## 2. 执行业务操作
## ...
## 3. 等待记录完成
## 4. 使用 JMC 打开 perf.jfr
jmc perf.jfr
## 5. 切换到 "代码" 标签,查看方法执行时间
## 输出示例:
## Method | Time (ms) | Samples
## --------------------------- | --------- | -------
## UserService.getUser() | 5000 | 1000
## CacheManager.get() | 3000 | 2000
## DatabaseService.query() | 2000 | 500
## 6. 发现 UserService.getUser() 耗时最长,定位优化2.4 图形化工具对比
┌────────────────────────────────────────────────────────────────────────────┐
│ 图形化诊断工具对比 │
├─────────────┬────────────────┬───────────────────────┬────────────────────┤
│ 工具 │ 开销 │ 主要功能 │ 适用场景 │
├─────────────┼────────────────┼───────────────────────┼────────────────────┤
│ JConsole │ 低(< 2%) │ 基础监控 │ 快速查看 JVM 状态 │
│ │ │ 内存、线程、类 │ 开发、测试环境 │
├─────────────┼────────────────┼───────────────────────┼────────────────────┤
│ VisualVM │ 中(抽样器高) │ 监控、分析、调优 │ 通用诊断工具 │
│ │ │ CPU/内存分析 │ 测试环境、生产监控 │
├─────────────┼────────────────┼───────────────────────┼────────────────────┤
│ JMC/JFR │ 极低(< 1%) │ 低开销监控、事件分析 │ 生产环境监控 │
│ │ │ 全方位性能分析 │ 性能调优 │
├─────────────┼────────────────┼───────────────────────┼────────────────────┤
│ VisualGC │ 低 │ GC 可视化 │ GC 分析 │
│ │ │ 垃圾回收可视化 │ GC 调优 │
└─────────────┴────────────────┴───────────────────────┴────────────────────┘
选择建议:
- 开发/测试环境:VisualVM(功能最全)
- 生产环境监控:JMC/JFR(开销最低)
- 快速查看:JConsole(最简单)
- GC 分析:VisualGC(专门分析 GC)三、现代诊断工具:Arthas
Arthas 是阿里巴巴开源的 Java 诊断工具,功能强大。Arthas 是线上排查问题的重要工具。
3.1 Arthas 简介
特点:
优势:
- 无需重启应用:在线诊断
- 功能丰富:查看类、方法、线程、GC 等
- 交互友好:Web Console
- 开源免费:Apache 2.0 协议
- 持续更新:社区活跃
适用场景:
- 线上问题排查
- 动态追踪方法调用
- 查看类加载信息
- 监控方法执行时间
- 反编译代码
- 修改日志级别3.2 安装和启动
方式 1:使用 arthas-boot(推荐)
## 下载 arthas-boot.jar
curl -O https://arthas.aliyun.com/arthas-boot.jar
## 启动 Arthas
java -jar arthas-boot.jar
## 输出示例:
## [INFO] arthas-boot version: 3.7.1
## [INFO] Found existing java process, please choose one and input the pid.
## [1] 12345 com.example.Application
## [2] 12346 org.gradle.launcher.daemon.bootstrap.GradleDaemon
## [3] 12347 sun.tools.jps.Jps
## Input the pid: 1
## 选择要诊断的进程编号,回车连接
## 直接指定 PID
java -jar arthas-boot.jar 12345方式 2:下载完整包
## 下载
wget https://arthas.aliyun.com/download/arthas-bin.zip
## 解压
unzip arthas-bin.zip -d arthas
## 启动
java -jar arthas/arthas-boot.jar方式 3:使用 Docker
## 拉取镜像
docker pull hengyunabc/arthas:latest
## 启动容器
docker run -it --rm \
--pid=container:<container_id> \
hengyunabc/arthas:latest \
java -jar /opt/arthas/arthas-boot.jar退出 Arthas:
## 退出当前会话(不停止 Arthas)
quit
## 完全退出 Arthas(停止 Arthas 服务)
stop3.3 常用命令
1. dashboard:仪表盘
## 查看综合仪表盘
dashboard
## 输出示例:
## Threads Memory Runtime
## ID NAME GROUP PRIORITY STATE %CPU used total max usage version 11.0.11
## 1 main main 5 TIMED_WAI 0.0 128M 256M 512M 25.00% pid 12345
## 2 Reference Handler system 10 WAITING 0.0
## 3 Finalizer system 8 WAITING 0.0 GARBAGE-COLLECTORS
## 4 Signal Dispatcher system 9 RUNNABLE 0.0 PS Scavenge [count : 15, time : 156ms]
## 5 Attach Listener system 5 RUNNABLE 0.0 PS MarkSweep [count : 2, time : 234ms]
## 按下 q 或 Ctrl+C 退出 dashboard仪表盘信息解读:
Threads 区域:
- ID:线程 ID
- NAME:线程名称
- GROUP:线程组
- PRIORITY:优先级
- STATE:线程状态
- %CPU:CPU 使用率
Memory 区域:
- used:已用内存
- total:总内存
- max:最大内存
- usage:使用率
Runtime 区域:
- version:JVM 版本
- pid:进程 ID
GARBAGE-COLLECTORS 区域:
- count:GC 次数
- time:GC 时间2. thread:线程命令
## 查看所有线程
thread
## 输出示例:
## Threads Total: 50, New: 0, Runnable: 10, Blocked: 5, Waiting: 20, TimedWaiting: 15
##
## ID NAME GROUP PRIORITY STATE %CPU TIME INTERRUPTED DAEMON
## 1 main main 5 TIMED_WAI 0.0 0:0.123 false false
## 2 Reference Handler system 10 WAITING 0.0 0:0.001 false true
## 3 Finalizer system 8 WAITING 0.0 0:0.002 false true
## 查看 CPU 使用率最高的 3 个线程
thread -n 3
## 输出示例:
## "http-nio-8080-exec-10" Id=123 cpuUsage=95.0% deltaTime=0.1s time=0:0:10
## at com.example.service.CalculateService.calculate(CalculateService.java:25)
## at com.example.controller.UserController.getUser(UserController.java:50)
## ...
## 查看特定线程
thread <thread_id>
## 查看阻塞状态的线程
thread -state BLOCKED
## 输出示例:
## "thread-1" Id=15 BLOCKED
## at com.example.Demo.method(Demo.java:25)
## - waiting to lock <0x00000000c0001234> (a java.lang.Object)
## - locked <0x00000000c0005678> (a java.lang.Object)
## 查看死锁
thread -b
## 输出示例:
## "thread-1" Id=15 BLOCKED
## at com.example.DeadlockDemo.method1(DeadlockDemo.java:20)
## - waiting to lock <0x00000000c0005678> (a java.lang.Object)
## - locked <0x00000000c0001234> (a java.lang.Object)
##
## "thread-2" Id=16 BLOCKED
## at com.example.DeadlockDemo.method2(DeadlockDemo.java:30)
## - waiting to lock <0x00000000c0001234> (a java.lang.Object)
## - locked <0x00000000c0005678> (a java.lang.Object)
##
## Found 1 deadlock.3. jvm:JVM 信息
## 查看 JVM 信息
jvm
## 输出示例:
## MEMORY
## MEMORY-MAX 512M
## MEMORY-TOTAL 256M
## MEMORY-USED 128M
## THREAD
## THREAD-COUNT 50
## DAEMON-COUNT 45
## PEAK-COUNT 60
## STARTED-COUNT 100
## DEADLOCK-COUNT 0
## GARBAGE-COLLECTORS
## PS Scavenge [count : 15, time : 156ms]
## PS MarkSweep [count : 2, time : 234ms]
## CLASS-LOADING
## LOADED-CLASS-COUNT 5000
## TOTAL-LOADED-CLASS-COUNT 5200
## UNLOADED-CLASS-COUNT 2004. watch:方法执行观察
作用: 观察方法执行,包括参数、返回值、异常、执行时间等。
## 基本语法
watch <类名> <方法名> <观察表达式> [条件表达式] [选项]
## 观察方法执行(查看参数和返回值)
watch com.example.service.UserService getUser "{params, returnObj}"
## 输出示例:
## method=com.example.service.UserService.getUser location=AtExit
## ts=2026-03-30 00:00:00; result=@ArrayList[
## @Object[][
## @Integer[123],
## ],
## @User[
## id=@Integer[123],
## name=@String[张三],
## age=@Integer[25],
## ],
## ]
## 观察方法执行时间
watch com.example.service.UserService getUser "{params, returnObj, #cost}" -x 2
## 输出示例:
## method=com.example.service.UserService.getUser location=AtExit
## ts=2026-03-30 00:00:00; result=@ArrayList[
## @Object[][
## @Integer[123],
## ],
## @User[
## id=@Integer[123],
## name=@String[张三],
## ],
## @Double[15.23], # 执行时间:15.23ms
## ]
## 观察方法异常
watch com.example.service.UserService getUser "{params, throwExp}" -e -x 2
## 输出示例:
## method=com.example.service.UserService.getUser location=AtExceptionExit
## ts=2026-03-30 00:00:00; result=@ArrayList[
## @Object[][
## @Integer[123],
## ],
## @UserNotFoundException[
## message=@String[用户不存在],
## ],
## ]
## 条件过滤(只观察特定参数)
watch com.example.service.UserService getUser "{params, returnObj}" "params[0]==123"
## 观察方法调用前(-b)
watch com.example.service.UserService getUser "{params, target}" -b -x 2
## 观察方法返回后(-s,默认)
watch com.example.service.UserService getUser "{params, returnObj}" -s -x 2
## 观察方法结束后(-f,正常返回或异常)
watch com.example.service.UserService getUser "{params, returnObj, throwExp}" -f -x 2watch 参数详解:
观察点:
-b : 方法调用前
-e : 方法异常后
-s : 方法返回后(默认)
-f : 方法结束后(正常返回或异常)
观察对象:
params : 方法参数数组
returnObj : 返回值
throwExp : 异常
target : 当前对象
this : 当前对象(同 target)
class : 当前类
method : 当前方法
#cost : 执行时间(ms)
#process : 当前处理时间(调用前为负数)
输出深度:
-x 1 : 输出第一层属性(默认)
-x 2 : 输出第二层属性
-x 3 : 输出第三层属性
-x 4 : 输出第四层属性
条件表达式:
"params[0]==123" : 第一个参数等于 123
"params[0].id>100" : 第一个参数的 id 属性大于 100
"#cost>100" : 执行时间大于 100ms
"throwExp!=null" : 有异常5. trace:方法调用追踪
作用: 追踪方法调用路径,显示每个方法的执行时间。
## 基本语法
trace <类名> <方法名> [条件表达式] [选项]
## 追踪方法调用路径
trace com.example.service.UserService getUser
## 输出示例:
## `---[15.23ms] com.example.service.UserService:getUser()
## +---[10.12ms] com.example.dao.UserDao:selectById() #23
## +---[3.45ms] com.example.cache.CacheManager:get() #24
## `---[1.66ms] com.example.util.ObjectUtil:convert() #25
## 解读:
## - getUser() 总耗时 15.23ms
## - selectById() 耗时 10.12ms,代码行号 #23
## - CacheManager.get() 耗时 3.45ms,代码行号 #24
## - ObjectUtil.convert() 耗时 1.66ms,代码行号 #25
## 追踪方法调用路径(包含 JDK 方法)
trace com.example.service.UserService getUser --skipJDKMethod false
## 条件过滤
trace com.example.service.UserService getUser "params[0]==123"
## 过滤执行时间大于 100ms 的调用
trace com.example.service.UserService getUser "#cost>100"
## 追踪多次调用
trace com.example.service.UserService getUser -n 5
## 保存追踪结果到文件
trace com.example.service.UserService getUser >> trace_output.txt6. stack:方法调用堆栈
作用: 查看方法被调用的完整堆栈。
## 查看方法调用堆栈
stack com.example.service.UserService getUser
## 输出示例:
## threadName=http-nio-8080-exec-1
## id=10
## methodName=getUser
## object=com.example.service.UserService@12345678
##
## at com.example.service.UserService.getUser(UserService.java:25)
## at com.example.controller.UserController.getUser(UserController.java:50)
## at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
## at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
## at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
## at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
## 条件过滤
stack com.example.service.UserService getUser "params[0]==123"7. jad:反编译
作用: 反编译类或方法,查看运行时代码。
## 反编译类
jad com.example.service.UserService
## 输出示例:
## ClassInfo:
## class-name: com.example.service.UserService
## code-source: /app/classes/
## classLoaderHash: 12345678
##
## public class UserService {
## public User getUser(Integer id) {
## User user = userDao.selectById(id);
## return user;
## }
## }
## 反编译特定方法
jad com.example.service.UserService getUser
## 反编译并显示行号
jad --lineNumber true com.example.service.UserService
## 反编译内部类
jad com.example.service.UserService\$InnerClass8. sc:查看类信息
## 查看类信息
sc com.example.service.UserService
## 输出示例:
## class-info com.example.service.UserService
## code-source /app/classes/
## name com.example.service.UserService
## isInterface false
## isAnnotation false
## isEnum false
## isAnonymousClass false
## isArray false
## isLocalClass false
## isMemberClass false
## isPrimitive false
## isSynthetic false
## simple-name UserService
## modifier public
## annotation
## interfaces
## super-class +-java.lang.Object
## class-loader +-sun.misc.Launcher$AppClassLoader@18b4aac2
## +-sun.misc.Launcher$ExtClassLoader@6108b2d7
## ClassLoader sun.misc.Launcher$AppClassLoader@18b4aac2
## 查看类字段
sc -d com.example.service.UserService
## 模糊搜索类
sc com.example.service.*9. sm:查看方法信息
## 查看方法信息
sm com.example.service.UserService
## 输出示例:
## com.example.service.UserService->getUser
## com.example.service.UserService->saveUser
## com.example.service.UserService->deleteUser
## 查看方法详细信息
sm -d com.example.service.UserService getUser
## 输出示例:
## declaring-class com.example.service.UserService
## method-name getUser
## modifier public
## annotation
## parameters java.lang.Integer
## return com.example.model.User
## exceptions
## classLoaderHash 12345678
## 模糊搜索方法
sm com.example.service.UserService get*10. monitor:方法调用统计
## 监控方法调用统计
monitor -c 5 com.example.service.UserService getUser
## 输出示例:
## timestamp class method total success fail avg-rt(ms) fail-rate
## --------------------------------------------------------------------------------------------
## 2026-03-30 00 UserService getUser 100 95 5 15.23 5.00%
## 2026-03-30 01 UserService getUser 120 118 2 12.45 1.67%
## 2026-03-30 02 UserService getUser 150 150 0 10.12 0.00%
## 参数说明:
## -c 5:每 5 秒输出一次统计
## total:调用次数
## success:成功次数
## fail:失败次数
## avg-rt:平均响应时间
## fail-rate:失败率11. profiler:性能分析
## 启动 CPU 性能分析
profiler start
## 输出示例:
## Started [cpu] profiling
## 执行业务操作...
## 停止性能分析
profiler stop
## 输出示例:
## CPU Profiling result:
## 25.12% com.example.service.CalculateService.calculate()
## 15.34% com.example.service.UserService.getUser()
## 10.23% com.example.cache.CacheManager.get()
## 启动内存分配分析
profiler start --event alloc
## 停止内存分配分析
profiler stop
## 输出示例:
## Memory Allocation Profiling result:
## 40.12% byte[]
## 25.34% java.lang.String
## 15.23% java.util.HashMap$Node
## 查看 profiler 状态
profiler status
## 输出示例:
## Profiling is running for 60 seconds3.4 Arthas 实战案例
案例 1:排查 CPU 高
场景: 线上 CPU 使用率 95%,需要快速定位。
## 1. 启动 Arthas
java -jar arthas-boot.jar
## 2. 查看仪表盘,确认 CPU 高
dashboard
## 输出示例:
## ID NAME GROUP PRIORITY STATE %CPU
## 123 http-nio-8080-exec-10 main 5 RUNNABLE 95.0
## 3. 查看 CPU 最高的 3 个线程
thread -n 3
## 输出示例:
## "http-nio-8080-exec-10" Id=123 cpuUsage=95.0% deltaTime=0.1s time=0:0:10
## at com.example.service.CalculateService.calculate(CalculateService.java:25)
## at com.example.controller.UserController.getUser(UserController.java:50)
## 4. 反编译查看代码
jad com.example.service.CalculateService calculate
## 输出示例:
## public int calculate(int n) {
## while (true) { // 死循环!
## n++;
## if (n < 0) break;
## }
## return n;
## }
## 5. 观察方法执行
watch com.example.service.CalculateService calculate "{params, returnObj, #cost}" -x 2
## 输出示例:
## method=calculate location=AtExit
## ts=2026-03-30 00:00:00; result=@ArrayList[
## @Object[][@Integer[1]],
## @Integer[Integer.MIN_VALUE],
## @Double[5000.12], # 执行时间 5 秒!
## ]解决方案: 修复死循环代码。
案例 2:排查方法慢
场景: 某个接口响应慢,需要定位瓶颈。
## 1. 启动 Arthas
java -jar arthas-boot.jar
## 2. 追踪方法调用
trace com.example.service.UserService getUser
## 输出示例:
## `---[123.45ms] com.example.service.UserService:getUser()
## +---[100.23ms] com.example.dao.UserDao:selectById() #23
## +---[20.12ms] com.example.cache.CacheManager:get() #24
## 3. 继续追踪慢的方法
trace com.example.dao.UserDao selectById
## 输出示例:
## `---[100.23ms] com.example.dao.UserDao:selectById()
## +---[80.12ms] java.sql.PreparedStatement:executeQuery() #15
## +---[15.23ms] java.sql.ResultSet:next() #18
## 4. 发现数据库查询慢,检查 SQL 或索引解决方案: 优化 SQL 或添加索引。
案例 3:动态修改日志级别
场景: 生产环境需要临时开启 DEBUG 日志排查问题。
## 1. 启动 Arthas
java -jar arthas-boot.jar
## 2. 查看当前日志级别
ognl '@org.slf4j.LoggerFactory@getLogger("com.example").getLevel()'
## 输出示例:
## @Level[INFO]
## 3. 修改日志级别为 DEBUG
ognl '@org.slf4j.LoggerFactory@getLogger("com.example").setLevel(@ch.qos.logback.classic.Level@DEBUG)'
## 4. 验证日志级别
ognl '@org.slf4j.LoggerFactory@getLogger("com.example").getLevel()'
## 输出示例:
## @Level[DEBUG]
## 5. 排查问题后恢复日志级别
ognl '@org.slf4j.LoggerFactory@getLogger("com.example").setLevel(@ch.qos.logback.classic.Level@INFO)'案例 4:排查内存泄漏
场景: 应用内存持续增长,疑似内存泄漏。
## 1. 启动 Arthas
java -jar arthas-boot.jar
## 2. 查看 JVM 内存信息
jvm
## 输出示例:
## MEMORY
## MEMORY-MAX 512M
## MEMORY-TOTAL 512M
## MEMORY-USED 480M # 使用率 93%!
## 3. 查看类统计
sc -d com.example.*
## 输出示例:
## class-info com.example.model.UserSession
## isInterface false
## ...
## 4. 查看对象数量
vmtool --action getInstances --className com.example.model.UserSession --limit 10
## 输出示例:
## @UserSession[][
## @UserSession[id=1, name=user1],
## @UserSession[id=2, name=user2],
## ...
## ]
## 5. 生成堆转储
heapdump /tmp/heap.hprof
## 输出示例:
## heapdump file created: /tmp/heap.hprof
## 6. 使用 MAT 分析 heap.hprof四、GC 日志分析
4.1 GC 日志配置
JDK 8 GC 日志配置:
## 开启 GC 日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:/logs/gc.log
## 完整配置
java \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-XX:+PrintGCTimeStamps \
-XX:+PrintGCApplicationStoppedTime \
-XX:+PrintGCApplicationConcurrentTime \
-XX:+PrintTenuringDistribution \
-Xloggc:/logs/gc.log \
-XX:+UseGCLogFileRotation \
-XX:NumberOfGCLogFiles=5 \
-XX:GCLogFileSize=10M \
-jar app.jarJDK 11+ GC 日志配置:
## 统一使用 -Xlog 参数
-Xlog:gc*:file=/logs/gc.log:time,level,tags:filecount=5,filesize=10m
## 完整配置
java \
-Xlog:gc*,gc+heap=debug,gc+age=trace:file=/logs/gc.log:time,level,tags:filecount=5,filesize=10m \
-jar app.jar4.2 GC 日志解读
Young GC 日志示例(JDK 8):
2026-03-30T00:00:00.123+0800: 15.234: [GC (Allocation Failure) [PSYoungGen: 262144K->32768K(305664K)] 262144K->32832K(1005568K), 0.0123456 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]
解读:
- 2026-03-30T00:00:00.123+0800: GC 发生时间
- 15.234: JVM 启动后的秒数
- [GC (Allocation Failure)]: GC 原因(分配失败)
- [PSYoungGen: ...]: Young GC
- 262144K->32768K: 新生代使用从 262MB 降到 32MB
- (305664K): 新生代总容量
- 262144K->32832K: 堆使用从 262MB 降到 32MB
- (1005568K): 堆总容量
- 0.0123456 secs: GC 耗时
- [Times: user=0.05 sys=0.01, real=0.01 secs]: 用户态、内核态、实际时间Full GC 日志示例(JDK 8):
2026-03-30T00:00:00.456+0800: 45.678: [Full GC (Ergonomics) [PSYoungGen: 32768K->0K(305664K)] [ParOldGen: 699392K->699392K(700416K)] 732160K->699392K(1006080K), [Metaspace: 51200K->51200K(1097728K)], 0.1234567 secs] [Times: user=1.23 sys=0.12, real=0.12 secs]
解读:
- [Full GC (Ergonomics)]: Full GC,原因是 Ergonomics(自动调优)
- [PSYoungGen: 32768K->0K]: 新生代从 32MB 降到 0
- [ParOldGen: 699392K->699392K]: 老年代没有变化(可能已满)
- 732160K->699392K: 堆使用从 732MB 降到 699MB
- [Metaspace: 51200K->51200K]: Metaspace 没有变化
- 0.1234567 secs: GC 耗时 123msG1 GC 日志示例(JDK 11+):
[0.123s][info][gc] GC(0) Pause Young (Allocation Failure) 25M->5M(100M) 10.123ms
[0.456s][info][gc] GC(1) Pause Young (Allocation Failure) 30M->10M(100M) 12.456ms
[1.234s][info][gc] GC(2) Pause Mixed (G1 Evacuation Pause) 80M->30M(100M) 45.678ms
[2.345s][info][gc] GC(3) Pause Full (Ergonomics) 95M->50M(100M) 123.456ms
解读:
- GC(0): GC 编号
- Pause Young: Young GC
- Pause Mixed: Mixed GC(G1 特有)
- Pause Full: Full GC
- 25M->5M: 回收前后堆使用
- (100M): 堆总大小
- 10.123ms: GC 耗时4.3 GC 日志分析工具
1. GCViewer(开源)
## 下载
wget https://github.com/chewiebug/GCViewer/releases/download/1.36/gcviewer-1.36.jar
## 使用
java -jar gcviewer-1.36.jar gc.log
## 功能:
## - 可视化 GC 日志
## - 统计 GC 次数、时间
## - 内存使用趋势图2. GCEasy(在线)
网址: https://gceasy.io/
使用方法:
1. 上传 GC 日志文件
2. 自动分析
3. 查看报告
报告内容:
- GC 统计(次数、时间)
- 内存使用趋势
- GC 停顿时间
- 内存泄漏检测
- 优化建议3. GCPlot(开源)
网址: https://github.com/gcplot/gcplot
功能:
- 批量分析 GC 日志
- 对比多次 GC 日志
- 生成详细报告4.4 GC 日志分析实战
案例 1:频繁 Full GC
GC 日志:
[10.123s][info][gc] GC(15) Pause Full (Ergonomics) 512M->500M(512M) 1234.567ms
[12.456s][info][gc] GC(16) Pause Full (Ergonomics) 512M->505M(512M) 1456.789ms
[14.789s][info][gc] GC(17) Pause Full (Ergonomics) 512M->510M(512M) 1678.901ms
分析:
- 频繁 Full GC(每 2-3 秒一次)
- GC 后内存使用不下降(500M->505M->510M)
- GC 时间越来越长(1234ms->1456ms->1678ms)
原因:
- 内存泄漏
- 堆内存不足
- 大对象过多
解决方案:
1. 增大堆内存(-Xmx1g)
2. 生成 heap dump 分析内存泄漏
3. 使用 G1 GC 减少停顿时间案例 2:Young GC 频繁
GC 日志:
[0.123s][info][gc] GC(0) Pause Young (Allocation Failure) 10M->2M(20M) 1.234ms
[0.234s][info][gc] GC(1) Pause Young (Allocation Failure) 10M->3M(20M) 1.567ms
[0.345s][info][gc] GC(2) Pause Young (Allocation Failure) 11M->4M(20M) 1.890ms
分析:
- Young GC 非常频繁(每 0.1 秒一次)
- 新生代只有 20MB,太小
- 对象快速填满 Eden 区
原因:
- 新生代太小
- 对象创建速度过快
解决方案:
1. 增大新生代(-Xmn256m)
2. 调整 NewRatio(-XX:NewRatio=1)
3. 优化代码减少对象创建五、线程 Dump 分析
5.1 生成线程 Dump
方式 1:使用 jstack
## 生成线程 Dump
jstack <pid> > thread_dump.txt
## 生成详细线程 Dump(包含锁信息)
jstack -l <pid> > thread_dump_detail.txt
## 强制生成(当进程无响应时)
jstack -F <pid> > thread_dump_force.txt方式 2:使用 jcmd
## 生成线程 Dump
jcmd <pid> Thread.print > thread_dump.txt
## 生成详细线程 Dump
jcmd <pid> Thread.print -l > thread_dump_detail.txt方式 3:使用 Arthas
## 启动 Arthas
java -jar arthas-boot.jar
## 生成线程 Dump
thread > thread_dump.txt
## 查看线程栈
thread <thread_id>方式 4:发送信号( Linux/Unix)
## 发送 SIGQUIT 信号
kill -3 <pid>
## 线程 Dump 会输出到标准输出或日志文件
## Tomcat:输出到 catalina.out
## Spring Boot:输出到控制台或日志文件方式 5:使用 VisualVM
1. 启动 VisualVM
2. 连接到 JVM 进程
3. 切换到 "线程" 标签
4. 点击 "Thread Dump" 按钮
5. 查看线程 Dump5.2 线程 Dump 分析技巧
技巧 1:统计线程状态
## 统计各种状态的线程数量
jstack <pid> | grep "java.lang.Thread.State" | sort | uniq -c | sort -rn
## 输出示例:
## 50 java.lang.Thread.State: TIMED_WAITING (sleeping)
## 30 java.lang.Thread.State: WAITING (on object monitor)
## 20 java.lang.Thread.State: RUNNABLE
## 10 java.lang.Thread.State: BLOCKED (on object monitor)
## 5 java.lang.Thread.State: WAITING (parking)
分析:
- RUNNABLE:正常状态,正在运行
- BLOCKED:锁竞争严重,需要优化
- WAITING:等待唤醒,可能是正常的
- TIMED_WAITING:睡眠或定时等待技巧 2:查找 BLOCKED 线程
## 查找所有 BLOCKED 线程
jstack <pid> | grep -B 2 "BLOCKED"
## 输出示例:
## "thread-1" Id=15 BLOCKED
## at com.example.Demo.method(Demo.java:25)
## 查找等待锁的线程
jstack <pid> | grep -A 5 "waiting to lock"
## 输出示例:
## at com.example.Demo.method(Demo.java:25)
## - waiting to lock <0x00000000c0001234> (a java.lang.Object)
## - locked <0x00000000c0005678> (a java.lang.Object)
## 查找持有锁的线程
jstack <pid> | grep -A 5 "locked <0x00000000c0001234>"技巧 3:检测死锁
## jstack 自动检测死锁
jstack <pid> | grep -A 30 "Found one Java-level deadlock"
## 或使用 Arthas
thread -b技巧 4:分析线程栈热点
## 提取所有线程栈中的方法调用
jstack <pid> | grep "at " | sort | uniq -c | sort -rn | head -20
## 输出示例:
## 50 at java.lang.Object.wait(Native Method)
## 30 at java.net.SocketInputStream.socketRead0(Native Method)
## 20 at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
## 10 at com.example.service.UserService.getUser(UserService.java:25)
分析:
- wait/notify:正常的等待/通知机制
- socketRead0:网络 I/O 等待
- park:Lock 锁等待
- 业务方法:需要重点关注5.3 线程 Dump 分析工具
1. TDA(Thread Dump Analyzer)
下载: https://github.com/iroise/tda
功能:
- 可视化线程 Dump
- 自动检测死锁
- 统计线程状态
- 查找阻塞线程2. FastThread(在线)
网址: https://fastthread.io/
使用方法:
1. 上传线程 Dump 文件
2. 自动分析
3. 查看报告
报告内容:
- 线程统计
- 死锁检测
- 阻塞线程
- CPU 高线程
- 优化建议3. Spotify Java Thread Dump Analyzer(在线)
网址: https://spotify.github.io/threaddump-analyzer/
功能:
- 在线分析
- 可视化展示
- 统计线程状态六、堆内存分析
6.1 生成堆转储
方式 1:使用 jmap
## 生成堆转储
jmap -dump:format=b,file=heap.hprof <pid>
## 只包含存活对象(会触发 Full GC)
jmap -dump:live,format=b,file=heap_live.hprof <pid>方式 2:使用 jcmd
## 生成堆转储
jcmd <pid> GC.heap_dump /tmp/heap.hprof方式 3:使用 Arthas
## 启动 Arthas
java -jar arthas-boot.jar
## 生成堆转储
heapdump /tmp/heap.hprof
## 只包含存活对象
heapdump --live /tmp/heap_live.hprof方式 4:OOM 时自动生成
## 启动时配置
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/heapdump.hprof -jar app.jar
## OOM 时自动生成堆转储到指定路径方式 5:使用 VisualVM
1. 启动 VisualVM
2. 连接到 JVM 进程
3. 右键进程 → Heap Dump
4. 或在监控面板点击 "Heap Dump" 按钮6.2 MAT 分析技巧
MAT(Memory Analyzer Tool)是最强大的堆转储分析工具。
安装 MAT
下载: https://eclipse.dev/mat/
安装:
1. 下载对应系统的版本
2. 解压
3. 运行 MemoryAnalyzer
注意:
- MAT 需要 JDK 8+ 环境
- 打开大堆转储文件需要调整 MAT 内存
- 编辑 MemoryAnalyzer.ini,添加 -Xmx4gMAT 分析功能
1. Leak Suspects Report(泄漏检测报告)
功能:
- 自动检测内存泄漏
- 显示疑似泄漏对象
使用:
1. 打开 heap dump 文件
2. 选择 "Leak Suspects Report"
3. 查看报告
报告内容:
- Problem Suspect 1:疑似泄漏点
- Problem Suspect 2:疑似泄漏点
- ...2. Dominator Tree(支配树)
功能:
- 查看对象支配树
- 找到占用内存最大的对象
使用:
1. 切换到 "Dominator Tree" 标签
2. 按大小排序
3. 找到占用内存最大的对象
4. 右键 → Path to GC Roots → exclude all phantom/weak/soft etc.
5. 查看引用链
技巧:
- 浅堆(Shallow Heap):对象本身占用内存
- 深堆(Retained Heap):对象及其引用对象占用内存
- 关注深堆大的对象3. Histogram(直方图)
功能:
- 按类统计对象数量和大小
使用:
1. 切换到 "Histogram" 标签
2. 按大小或数量排序
3. 找到对象数量异常或大小异常的类
技巧:
- 关注对象数量异常的类
- 关注占用内存最大的类
- 对比多个 heap dump,找出增长的对象4. OQL(Object Query Language)
功能:
- 使用 SQL 语法查询对象
语法:
SELECT <选择列表>
FROM <类名>
WHERE <条件>
示例:
## 查询长度大于 1000 的字符串
SELECT s FROM java.lang.String s WHERE s.count > 1000
## 查询所有 UserSession 对象
SELECT s FROM com.example.model.UserSession s
## 查询特定 ID 的用户
SELECT u FROM com.example.model.User u WHERE u.id = 123
## 统计对象数量
SELECT COUNT(s) FROM java.lang.String sMAT 实战案例
案例 1:查找内存泄漏
## 1. 打开 heap dump 文件
## 2. 选择 "Leak Suspects Report"
## 输出示例:
## Problem Suspect 1:
## The class java.util.HashMap$Node[], loaded by <system class loader>,
## occupies 500.00MB (50.00%) bytes. The instance of java.util.HashMap$Node[]
## is referenced by com.example.cache.SessionCache.sessions.
## 3. 点击 "Details" 查看详情
## 4. 切换到 "Dominator Tree"
## 5. 找到 SessionCache.sessions
## 6. 右键 → Path to GC Roots → exclude all phantom/weak/soft etc.
## 输出示例:
## GC Root: static field SessionCache.sessions
## └── HashMap$Node[]
## └── HashMap$Node
## └── UserSession (50000 instances)
## 7. 定位到 SessionCache.sessions 字段
## 8. 发现缓存未清理,导致内存泄漏案例 2:查找大对象
## 1. 切换到 "Dominator Tree"
## 2. 按 Retained Heap 排序
## 输出示例:
## Class | Shallow Heap | Retained Heap
## -------------------- | ------------ | -------------
## byte[10000000] | 10,000,016 | 10,000,016
## UserSession[50000] | 200,000 | 500,000,000
## ArrayList | 24 | 50,000,024
## 3. 发现 UserSession 数组占用 500MB
## 4. 查看 UserSession 对象详情
## 5. 切换到 "Histogram"
## 6. 按 Retained Heap 排序
## 输出示例:
## Class | Objects | Shallow Heap | Retained Heap
## -------------------- | --------- | ------------ | -------------
## UserSession | 50,000 | 2,000,000 | 500,000,000
## byte[] | 10,000 | 100,000,000 | 100,000,000
## String | 100,000 | 2,400,000 | 50,000,000
## 7. 发现 UserSession 对象数量异常(50000 个)
## 8. 查看引用链,找到持有这些对象的地方6.3 在线堆分析工具
1. Heaphero(在线)
网址: https://heaphero.io/
使用方法:
1. 上传 heap dump 文件
2. 自动分析
3. 查看报告
报告内容:
- 内存使用统计
- 对象统计
- 内存泄漏检测
- 优化建议2. GCEasy(在线)
网址: https://gceasy.io/
功能:
- GC 日志分析
- 堆 dump 分析七、线上问题排查完整流程
7.1 问题分类与排查入口
┌────────────────────────────────────────────────────────────────────────────┐
│ 线上问题排查入口 │
├─────────────┬──────────────────────┬──────────────────────────────────────┤
│ 问题类型 │ 现象 │ 排查入口 │
├─────────────┼──────────────────────┼──────────────────────────────────────┤
│ CPU 飙高 │ CPU 使用率持续高 │ top、jstack、thread -n、Arthas │
├─────────────┼──────────────────────┼──────────────────────────────────────┤
│ 内存问题 │ 内存持续增长、OOM │ jstat、jmap、heapdump、MAT │
├─────────────┼──────────────────────┼──────────────────────────────────────┤
│ GC 问题 │ 频繁 GC、停顿长 │ jstat、GC 日志、jcmd │
├─────────────┼──────────────────────┼──────────────────────────────────────┤
│ 线程问题 │ 死锁、阻塞、超时 │ jstack、thread -b、thread -state │
├─────────────┼──────────────────────┼──────────────────────────────────────┤
│ 类加载问题 │ ClassNotFoundException │ jcmd、classloader、sc │
├─────────────┼──────────────────────┼──────────────────────────────────────┤
│ 性能问题 │ 响应慢、吞吐低 │ trace、watch、profiler │
└─────────────┴──────────────────────┴──────────────────────────────────────┘7.2 CPU 飙高排查流程
完整排查步骤
## 步骤 1:确认 CPU 高的进程
top
## 输出示例:
## PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
## 12345 appuser 20 0 4.0g 1.2g 120m S 95.0 7.5 0:10.23 java
## 步骤 2:查看高 CPU 的线程
top -Hp 12345
## 输出示例:
## PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
## 12346 appuser 20 0 4.0g 1.2g 120m R 90.0 7.5 0:09.15 java
## 12347 appuser 20 0 4.0g 1.2g 120m S 2.0 7.5 0:00.10 java
## 步骤 3:转换线程 ID 为十六进制
printf "%x\n" 12346
## 输出: 303a
## 步骤 4:查看线程栈
jstack 12345 | grep -A 20 "nid=0x303a"
## 或使用 Arthas
java -jar arthas-boot.jar 12345
thread -n 3
## 步骤 5:定位到代码
jad com.example.service.CalculateService calculate常见原因及解决
┌──────────────────────────────────────────────────────────────────────┐
│ CPU 高常见原因 │
├──────────────────────────────────────────────────────────────────────┤
│ 1. 业务代码问题 │
│ - 死循环 │
│ - 复杂计算 │
│ - 正则表达式回溯 │
│ 解决:优化代码、算法优化 │
│ │
│ 2. GC 频繁 │
│ - 内存不足 │
│ - 内存泄漏 │
│ 解决:增大内存、优化 GC 参数、修复内存泄漏 │
│ │
│ 3. 线程问题 │
│ - 线程数过多 │
│ - 锁竞争 │
│ 解决:优化线程池、减少锁竞争 │
└──────────────────────────────────────────────────────────────────────┘7.3 内存泄漏排查流程
完整排查步骤
## 步骤 1:查看内存使用趋势
jstat -gcutil <pid> 1000 10
## 输出示例:
## S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
## 0.00 85.00 55.00 75.00 85.00 75.00 150 1.500 2 0.500 2.000
## 0.00 85.00 60.00 78.00 85.00 75.00 152 1.520 2 0.500 2.020
## 0.00 85.00 65.00 80.00 85.00 75.00 154 1.540 2 0.500 2.040
## 老年代持续增长,说明可能有内存泄漏
## 步骤 2:查看堆内存详情
jmap -heap <pid>
## 步骤 3:查看对象统计
jmap -histo <pid> | head -25
## 步骤 4:生成堆转储
jmap -dump:format=b,file=heap.hprof <pid>
## 或使用 jcmd
jcmd <pid> GC.heap_dump /tmp/heap.hprof
## 或使用 Arthas
heapdump /tmp/heap.hprof
## 步骤 5:使用 MAT 分析堆转储MAT 分析步骤
1. 打开 heap dump 文件
2. 选择 "Leak Suspects Report"
3. 查看疑似泄漏点
4. 切换到 "Dominator Tree"
5. 找到占用内存最大的对象
6. 右键 → Path to GC Roots → exclude all phantom/weak/soft etc.
7. 查看引用链
8. 定位到代码7.4 线程死锁排查流程
完整排查步骤
## 步骤 1:查看线程栈
jstack <pid> | grep -A 10 "Found one Java-level deadlock"
## 或使用 Arthas
thread -b
## 步骤 2:分析死锁线程
jstack <pid> > thread_dump.txt
## 查看死锁详情
grep -A 30 "Found one Java-level deadlock" thread_dump.txt
## 步骤 3:定位死锁代码
## 从线程栈中找到持有锁的代码位置死锁示例分析
// 死锁代码示例
public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try { Thread.sleep(10); } catch (InterruptedException e) {}
synchronized (lock2) { // 等待 lock2,但 lock2 被 Thread 2 持有
System.out.println("Thread 1: Holding lock 1 & 2...");
}
}
}
public void method2() {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try { Thread.sleep(10); } catch (InterruptedException e) {}
synchronized (lock1) { // 等待 lock1,但 lock1 被 Thread 1 持有
System.out.println("Thread 2: Holding lock 1 & 2...");
}
}
}
}解决方案:
// 方案 1:统一锁顺序
public void transfer(Account from, Account to, BigDecimal amount) {
Object firstLock = from.getId() < to.getId() ? lockA : lockB;
Object secondLock = from.getId() < to.getId() ? lockB : lockA;
synchronized (firstLock) {
synchronized (secondLock) {
from.debit(amount);
to.credit(amount);
}
}
}
// 方案 2:使用超时锁
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try {
from.debit(amount);
to.credit(amount);
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}
// 方案 3:使用 Lock 的公平锁
private final Lock lock1 = new ReentrantLock(true);
private final Lock lock2 = new ReentrantLock(true);7.5 频繁 GC 排查流程
完整排查步骤
## 步骤 1:查看 GC 统计
jstat -gcutil <pid> 1000 10
## 步骤 2:查看 GC 日志
tail -f /logs/gc.log
## 或使用 GC 日志分析工具
## 上传到 https://gceasy.io/
## 步骤 3:分析 GC 原因
## - Allocation Failure:内存分配失败,正常
## - System.gc():代码调用 System.gc(),建议移除
## - CMS Concurrent Mark:CMS GC 正常
## - Ergonomics:自动调优,可能是内存不足
## 步骤 4:根据原因优化
## - 内存不足:增大堆内存
## - 新生代太小:增大新生代
## - 对象创建过快:优化代码
## - 内存泄漏:修复内存泄漏八、生产环境排查案例
8.1 案例 1:CPU 飙高 - 死循环
问题现象:
服务 CPU 使用率持续 95%+,响应缓慢。排查过程:
## 1. 找到高 CPU 进程
top
## 输出:
## PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
## 12345 appuser 20 0 4.0g 1.2g 120m S 95.0 7.5 0:10.23 java
## 2. 找到高 CPU 线程
top -Hp 12345
## 输出:
## PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
## 12346 appuser 20 0 4.0g 1.2g 120m R 90.0 7.5 0:09.15 java
## 3. 转换线程 ID
printf "%x\n" 12346
## 输出: 303a
## 4. 查看线程栈
jstack 12345 | grep -A 20 "nid=0x303a"
## 输出:
## "http-nio-8080-exec-10" Id=123 runnable
## at com.example.service.CalculateService.calculate(CalculateService.java:25)
## 5. 反编译代码
jad com.example.service.CalculateService calculate定位问题:
public class CalculateService {
public int calculate(int n) {
// 死循环:条件永远不满足
while (true) {
n++;
if (n < 0) break; // int 溢出后才会 < 0
}
return n;
}
}解决方案:
public class CalculateService {
public int calculate(int n) {
// 添加循环退出条件
int maxIterations = 10000;
int count = 0;
while (count < maxIterations) {
n++;
count++;
}
return n;
}
}8.2 案例 2:内存泄漏 - 缓存未清理
问题现象:
服务运行一段时间后频繁 Full GC,内存持续增长。排查过程:
## 1. 查看 GC 统计
jstat -gcutil 12345 1000 5
## 输出:
## S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
## 0.00 85.00 55.00 75.00 85.00 75.00 150 1.500 2 0.500 2.000
## 0.00 85.00 60.00 78.00 85.00 75.00 152 1.520 2 0.500 2.020
## 老年代持续增长,说明可能有内存泄漏
## 2. 生成堆转储
jmap -dump:format=b,file=leak.hprof 12345
## 3. 使用 MAT 分析
## 打开 leak.hprof,查看 Leak Suspects Report
## 输出:
## Problem Suspect 1:
## The class java.util.HashMap$Node[], occupies 500.00MB (50.00%) bytes.
## Referenced by com.example.cache.SessionCache.sessions.定位问题:
public class CacheManager {
// 缓存只增不减,导致内存泄漏
private static Map<String, Object> cache = new HashMap<>();
public static void put(String key, Object value) {
cache.put(key, value);
}
// 缺少 remove 或清理机制
}解决方案:
import java.util.concurrent.TimeUnit;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
public class CacheManager {
// 使用 Guava Cache,设置过期时间和最大容量
private static Cache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(10000) // 最大缓存数量
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后 10 分钟过期
.recordStats() // 记录统计信息
.build();
public static void put(String key, Object value) {
cache.put(key, value);
}
public static Object get(String key) {
return cache.getIfPresent(key);
}
public static void remove(String key) {
cache.invalidate(key);
}
public static void clear() {
cache.invalidateAll();
}
}8.3 案例 3:频繁 Young GC
问题现象:
Young GC 频繁(每秒 10+ 次),吞吐量下降。排查过程:
## 1. 查看 GC 统计
jstat -gcutil 12345 1000 5
## 输出:
## S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
## 0.00 85.00 95.00 45.00 85.00 75.00 150 1.500 0 0.000 1.500
## 0.00 85.00 5.00 45.00 85.00 75.00 151 1.510 0 0.000 1.510 # GC 后
## 0.00 85.00 15.00 45.00 85.00 75.00 151 1.510 0 0.000 1.510 # 快速增长
## 0.00 85.00 25.00 45.00 85.00 75.00 151 1.510 0 0.000 1.510
## 0.00 85.00 35.00 45.00 85.00 75.00 151 1.510 0 0.000 1.510
## Eden 区快速增长,每秒 10+ 次 Young GC
## 2. 查看新生代大小
jinfo -flag NewSize 12345
## 输出:
## -XX:NewSize=10485760 # 新生代只有 10MB,太小!
## 3. 使用 Arthas 观察对象创建
prof start --event alloc
## 执行业务操作...
## 4. 查看分配热点
prof stop
## 输出:
## Memory Allocation Profiling result:
## 40.12% byte[]
## 25.34% java.lang.String
## 15.23% com.example.model.TempData定位问题:
public class DataProcessService {
public void process(List<String> dataList) {
// 每次处理创建大量临时对象
for (String data : dataList) {
byte[] temp = new byte[1024 * 1024]; // 1MB 临时数组
// 处理数据
}
}
}解决方案:
public class DataProcessService {
// 方案 1:重用临时数组
private static final ThreadLocal<byte[]> tempBuffer =
ThreadLocal.withInitial(() -> new byte[1024 * 1024]);
public void process(List<String> dataList) {
byte[] temp = tempBuffer.get();
for (String data : dataList) {
// 重用 temp 数组
// 处理数据
}
}
}
// 方案 2:增大新生代
// -Xmn512m # 新生代从 10MB 增加到 512MB参数调优:
## 增大新生代
-Xmn512m # 新生代从 10MB 增加到 512MB
## 或调整新生代比例
-XX:NewRatio=1 # 老年代:新生代 = 1:1(默认是 2:1)
## 或使用 G1 收集器
-XX:+UseG1GC
-XX:MaxGCPauseMillis=2008.4 案例 4:线程死锁
问题现象:
应用无响应,请求超时。排查过程:
## 1. 查看线程栈
jstack 12345
## 输出:
## Found one Java-level deadlock:
## =============================
## "thread-2":
## waiting to lock monitor 0x00007f8c0c00a800 (object 0x00000000c0001234),
## which is held by "thread-1"
## "thread-1":
## waiting to lock monitor 0x00007f8c0c00b800 (object 0x00000000c0005678),
## which is held by "thread-2"
##
## Java stack information for the threads listed above:
## ===================================================
## "thread-2":
## at com.example.DeadlockDemo.method2(DeadlockDemo.java:30)
## - waiting to lock <0x00000000c0001234> (a java.lang.Object)
## - locked <0x00000000c0005678> (a java.lang.Object)
## "thread-1":
## at com.example.DeadlockDemo.method1(DeadlockDemo.java:20)
## - waiting to lock <0x00000000c0005678> (a java.lang.Object)
## - locked <0x00000000c0001234> (a java.lang.Object)
##
## Found 1 deadlock.定位问题:
public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
synchronized (lock2) {
// 业务逻辑
}
}
}
public void method2() {
synchronized (lock2) {
synchronized (lock1) {
// 业务逻辑
}
}
}
}解决方案:
public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
// 方案 1:统一锁顺序
public void method1() {
synchronized (lock1) {
synchronized (lock2) {
// 业务逻辑
}
}
}
public void method2() {
synchronized (lock1) { // 改为和 method1 相同的锁顺序
synchronized (lock2) {
// 业务逻辑
}
}
}
// 方案 2:使用 ReentrantLock
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void method3() {
try {
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lock2.tryLock(1, TimeUnit.SECONDS)) {
try {
// 业务逻辑
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}十、最佳实践
10.1 监控预警
┌──────────────────────────────────────────────────────────────────────┐
│ 监控指标建议 │
├──────────────────────────────────────────────────────────────────────┤
│ 1. CPU 监控 │
│ - CPU 使用率 > 80%:预警 │
│ - CPU 使用率 > 90%:告警 │
│ │
│ 2. 内存监控 │
│ - 堆内存使用率 > 80%:预警 │
│ - 堆内存使用率 > 90%:告警 │
│ - 老年代使用率 > 80%:预警 │
│ - Metaspace 使用率 > 80%:预警 │
│ │
│ 3. GC 监控 │
│ - Young GC 频率 > 10 次/秒:预警 │
│ - Full GC 频率 > 1 次/小时:预警 │
│ - Full GC 频率 > 1 次/分钟:告警 │
│ - GC 停顿时间 > 500ms:预警 │
│ - GC 停顿时间 > 1s:告警 │
│ │
│ 4. 线程监控 │
│ - 线程数 > 500:预警 │
│ - 线程数 > 1000:告警 │
│ - 死锁检测:告警 │
│ - BLOCKED 线程 > 10:预警 │
│ │
│ 5. 响应时间监控 │
│ - P95 > 1s:预警 │
│ - P95 > 3s:告警 │
│ - 错误率 > 1%:预警 │
│ - 错误率 > 5%:告警 │
└──────────────────────────────────────────────────────────────────────┘10.2 JVM 参数配置建议
小型应用(1-2GB 堆):
java \
-Xms1g \
-Xmx1g \
-Xmn512m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/data/logs/heapdump.hprof \
-Xlog:gc*:file=/data/logs/gc.log:time,level,tags \
-jar app.jar中型应用(4-8GB 堆):
java \
-Xms4g \
-Xmx4g \
-Xmn2g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:InitiatingHeapOccupancyPercent=40 \
-XX:G1HeapRegionSize=8m \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/data/logs/heapdump.hprof \
-Xlog:gc*:file=/data/logs/gc.log:time,level,tags:filecount=5,filesize=100m \
-jar app.jar大型应用(16GB+ 堆):
java \
-Xms16g \
-Xmx16g \
-XX:+UseZGC \
-XX:ZCollectionInterval=5 \
-XX:ConcGCThreads=4 \
-XX:ParallelGCThreads=8 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/data/logs/heapdump.hprof \
-Xlog:gc*:file=/data/logs/gc.log:time,level,tags:filecount=10,filesize=100m \
-jar app.jar10.3 排障检查清单
┌──────────────────────────────────────────────────────────────────────┐
│ 排障检查清单 │
├──────────────────────────────────────────────────────────────────────┤
│ □ 1. 确认问题类型 │
│ □ CPU 高 │
│ □ 内存增长/OOM │
│ □ GC 频繁 │
│ □ 线程阻塞/死锁 │
│ □ 响应慢 │
│ │
│ □ 2. 保留现场 │
│ □ 查看监控图表 │
│ □ 保存线程快照 │
│ □ 保存 GC 日志 │
│ □ 必要时生成堆转储 │
│ □ 保存系统日志 │
│ │
│ □ 3. 选择工具 │
│ □ CPU 问题:top、jstack、thread │
│ □ 内存问题:jstat、jmap、MAT │
│ □ GC 问题:jstat、GC 日志 │
│ □ 线程问题:jstack、thread │
│ │
│ □ 4. 分析根因 │
│ □ 定位到具体代码 │
│ □ 分析问题原因 │
│ □ 制定解决方案 │
│ │
│ □ 5. 验证修复 │
│ □ 测试环境验证 │
│ □ 生产环境灰度发布 │
│ □ 监控确认问题解决 │
│ □ 文档记录问题和解决方案 │
└──────────────────────────────────────────────────────────────────────┘10.4 预防措施
1. 代码层面
- 避免内存泄漏(及时清理缓存、关闭资源)
- 避免死循环和复杂计算
- 合理使用线程池
- 避免嵌套锁
2. 配置层面
- 合理配置 JVM 参数
- 开启 GC 日志
- 开启 HeapDumpOnOutOfMemoryError
- 配置监控预警
3. 测试层面
- 压力测试
- 内存泄漏检测
- 性能测试
- 代码审查
4. 监控层面
- 应用性能监控(APM)
- JVM 监控
- 系统监控
- 日志监控
5. 应急预案
- 制定排障流程
- 准备排障脚本
- 培训团队成员
- 定期演练版本差异(旧版 → Java 21)
| 特性 | 旧版(JDK 8/11) | Java 21 |
|---|---|---|
| 命令行工具 | jps/jstack/jmap/jstat | 不变;jcmd 合并多数诊断能力 |
| GC 日志 | -Xloggc(8) | -Xlog:gc*(9+,不变) |
| 堆转储 | jmap -dump | jcmd GC.heap_dump(推荐) |
| 虚拟线程诊断 | 无 | jstack 标识虚拟线程,需区分 carrier |
| 强封装 | 反射可访问内部 API | JDK 17+ 默认强封装,诊断工具需适配 |
| 工具版本 | JDK 8/11 命令差异大 | JDK 21 建议统一使用 jcmd + 容器感知 |