文章目录
前言
JVM性能监控与故障排查工具是解决线上问题的利器。CPU突然飙升100%怎么排查?内存泄漏如何定位?死锁如何发现?线程一直WAITING在哪里? 理解jps的进程查看、jstat的实时监控、jmap的内存分析、jstack的线程快照、VisualVM的可视化监控,才能掌握线上问题快速定位的本质。
摘要
从"CPU 100%导致服务假死"的故障出发,剖析JVM性能监控与排查的核心工具。通过jstat的GC实时监控、jmap的堆内存dump、jstack的线程状态分析、MAT的内存泄漏定位、Arthas的在线诊断,揭秘从发现问题到定位根因的完整流程。配合详细命令示例与实战案例,给出线上问题排查的最佳实践。
一、从CPU 100%说起
周五下午5点,哈吉米的服务突然卡死:
场景1:CPU 100%,服务无响应
监控告警:
CPU使用率:100%(4核全满)
接口响应:超时
用户投诉:打不开页面
哈吉米(慌):“什么情况?CPU打满了?”
登录服务器查看:
top
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12345 root 20 0 4.5g 2.3g 10m R 398.7 58.2 10:23 java
# CPU 398.7%(4核 × 100%)
哈吉米:“Java进程CPU 400%,肯定是代码有问题。但是哪里有问题?”
南北绿豆赶来:“用jstack看线程在干什么。”
# 1. 找到CPU最高的线程
top -Hp 12345
PID %CPU
12346 99.8 ← 这个线程CPU最高
12347 0.3
12348 0.1
# 2. 转为16进制(jstack用16进制显示线程ID)
printf "%x\n" 12346
# 输出:303a
# 3. jstack查看这个线程在干什么
jstack 12345 | grep 303a -A 30
"http-nio-8080-exec-1" #123 daemon prio=5 tid=0x303a
java.lang.Thread.State: RUNNABLE
at com.example.service.UserService.exportAllUsers(UserService.java:45)
at com.example.controller.UserController.export(UserController.java:30)
南北绿豆:“找到了!UserService.exportAllUsers这个方法CPU 100%。”
查看代码:
public void exportAllUsers() {
List<User> users = userMapper.selectAll(); // 查询500万用户
StringBuilder sb = new StringBuilder();
for (User user : users) {
sb.append(user.toString()); // 字符串拼接500万次
sb.append("\n");
}
return sb.toString();
}
// 问题:
// 500万次字符串拼接
// StringBuilder频繁扩容
// CPU 100%
哈吉米:“原来是这里!”
阿西噶阿西:“来,我教你完整的排查流程和工具使用。”
二、JVM命令行工具
2.1 jps(查看Java进程)
命令:
jps
# 输出:
12345 Application
23456 jar
34567 Jps
# 详细信息
jps -l
12345 com.example.Application
23456 /opt/app/app.jar
# 查看JVM参数
jps -v
12345 Application -Xmx4g -Xms4g -XX:+UseG1GC
常用参数:
| 参数 | 说明 |
|---|---|
| -l | 显示主类全名或jar路径 |
| -v | 显示JVM参数 |
| -m | 显示main方法参数 |
哈吉米:“jps就是Java版的ps,查看Java进程。”
2.2 jstat(实时监控)
监控GC:
jstat -gc 12345 1000 10
# 每1秒输出一次,共10次
S0C S1C S0U S1U EC EU OC OU MC MU
102400 102400 51200 0.0 819200 614400 2097152 1572864 51200 48000
YGCT YGCT FGC FGCT GCT
150 15.234 5 25.123 40.357
字段说明:
S0C:Survivor0容量(KB)
S0U:Survivor0使用(KB)
EC:Eden容量
EU:Eden使用
OC:Old老年代容量
OU:Old老年代使用
YGCT:Young GC总耗时(秒)
FGCT:Full GC总耗时(秒)
GCT:GC总耗时
监控GC百分比:
jstat -gcutil 12345 1000 10
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 50.00 75.00 75.00 93.75 90.00 150 15.234 5 25.123 40.357
0.00 50.00 80.00 80.00 93.75 90.00 150 15.234 5 25.123 40.357
0.00 50.00 90.00 90.00 93.75 90.00 150 15.234 5 25.123 40.357
0.00 50.00 95.00 95.00 93.75 90.00 151 15.456 5 25.123 40.579
↑ Eden使用率上升 ↑ YGC +1
问题:
Eden使用率持续上升 → 对象创建过快
YGC频繁 → 年轻代空间不足
监控类加载:
jstat -class 12345
Loaded Bytes Unloaded Bytes Time
12345 23456 100 200 120.5
# Loaded:加载的类数量
# Unloaded:卸载的类数量(持续为0正常,持续增长异常)
常用参数:
| 参数 | 说明 |
|---|---|
| -gc | GC统计(容量和使用量) |
| -gcutil | GC统计(百分比) |
| -gccause | GC统计 + GC原因 |
| -class | 类加载统计 |
南北绿豆:“jstat是实时监控的利器,能快速发现问题。”
2.3 jmap(内存分析)
功能1:dump堆内存
# 导出堆快照(会触发Full GC)
jmap -dump:live,format=b,file=heap.hprof 12345
# 参数说明:
# live:只导出存活对象(触发Full GC)
# format=b:二进制格式
# file:输出文件
# 导出后用MAT分析
# 文件大小约为堆内存的60-80%
功能2:查看堆配置:
jmap -heap 12345
# 输出:
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 4294967296 (4096.0MB)
NewSize = 1431633920 (1365.3MB)
MaxNewSize = 1431633920 (1365.3MB)
OldSize = 2863333376 (2730.7MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.8MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 1073741824 (1024.0MB)
used = 858993459 (819.2MB)
free = 214748365 (204.8MB)
80.0% used
功能3:查看对象统计:
jmap -histo 12345 | head -20
# 输出:
num #instances #bytes class name
----------------------------------------------
1: 1234567 123456789 [C ← char数组
2: 1000000 100000000 java.lang.String
3: 500000 50000000 com.example.entity.User
4: 300000 30000000 java.util.HashMap$Node
5: 200000 20000000 byte[]
分析:
User对象有50万个,占50MB
如果不合理 → 可能泄漏
阿西噶阿西:“jmap能看到内存使用详情,定位内存泄漏。”
2.4 jstack(线程分析)
功能1:导出线程快照
jstack 12345 > thread.txt
# 查看所有线程状态
cat thread.txt
"http-nio-8080-exec-1" #12 daemon prio=5 tid=0x7f8a1c004e00 nid=0x303a waiting on condition
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park
at com.example.service.UserService.method1(UserService.java:30)
线程状态:
| 状态 | 说明 | 原因 |
|---|---|---|
| RUNNABLE | 运行中 | 正常 |
| WAITING | 等待中 | wait()、park() |
| TIMED_WAITING | 限时等待 | sleep()、wait(timeout) |
| BLOCKED | 阻塞 | 等待获取synchronized锁 |
功能2:检测死锁
jstack 12345
# 输出:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x7f8a1c004e00 (object 0xd5f20000),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x7f8a1c002e00 (object 0xd5f20010),
which is held by "Thread-1"
功能3:找CPU高的线程
# 结合top使用
top -Hp 12345 # 找到CPU高的线程ID
# 转为16进制
printf "%x\n" 12346
# 303a
# jstack查看
jstack 12345 | grep 303a -A 30
南北绿豆:“jstack能看到线程在干什么,定位死锁、死循环、阻塞问题。”
2.5 jinfo(查看和修改JVM参数)
查看参数:
# 查看所有JVM参数
jinfo -flags 12345
# 查看某个参数
jinfo -flag MaxHeapSize 12345
# 输出:-XX:MaxHeapSize=4294967296
# 查看系统属性
jinfo -sysprops 12345
动态修改参数:
# 修改GC日志开关(部分参数支持动态修改)
jinfo -flag +PrintGC 12345 # 开启
jinfo -flag -PrintGC 12345 # 关闭
# 修改堆大小(不支持,需要重启)
jinfo -flag MaxHeapSize=8g 12345
# 报错:Cannot change MaxHeapSize
可动态修改的参数:
| 参数 | 是否可动态修改 |
|---|---|
| PrintGC | ✅ 可以 |
| HeapDumpOnOutOfMemoryError | ✅ 可以 |
| MaxHeapSize | ❌ 不可以 |
| UseG1GC | ❌ 不可以 |
哈吉米:“jinfo可以查看参数,部分参数还能动态修改。”
三、可视化监控工具
3.1 VisualVM(JDK自带)
启动:
# JDK 8及以前自带
jvisualvm
# JDK 9后需要单独下载
# https://visualvm.github.io/
核心功能:
1. 监控
- 实时查看CPU、内存、类、线程
- 图形化展示
2. 线程
- 查看所有线程状态
- 线程dump
- 死锁检测
3. 抽样器
- CPU抽样(找到耗CPU的方法)
- 内存抽样(找到占内存的对象)
4. Profiler
- CPU性能分析
- 内存性能分析
监控面板示例:
CPU使用率:
████████████████████░░ 80%
堆内存:
已使用:2.3GB / 4GB
████████████████░░░░░░ 58%
类加载:
已加载:12,345个
线程:
活动线程:150个
守护线程:50个
3.2 MAT(Memory Analyzer Tool)
分析堆快照:
1. File → Open Heap Dump → 选择heap.hprof
2. 自动生成Leak Suspects Report(泄漏疑点报告)
3. 查看Histogram(对象统计)
Class Name Objects Shallow Heap
-------------------------------------------------
java.lang.String 1,000,000 24,000,000
com.example.entity.User 500,000 16,000,000
byte[] 300,000 50,000,000
4. 查看Dominator Tree(支配树)
找到占内存最多的对象
5. 查看GC Roots路径
右键对象 → Path to GC Roots
查看引用链
Leak Suspects报告示例:
Problem Suspect 1:
The thread "http-nio-8080-exec-1"
occupies 1.2 GB (60% of total heap)
Details:
- com.example.cache.LocalCache.dataMap
└─ HashMap with 500,000 entries
└─ occupies 1.2 GB
Shortest Paths From GC Roots:
LocalCache (Class)
→ dataMap (static field)
→ HashMap
→ 500,000 User objects
Suggestion:
LocalCache.dataMap缓存500万个User对象
没有过期机制,导致内存泄漏
建议:使用软引用 + LRU淘汰
南北绿豆:“MAT的Leak Suspects能自动分析泄漏,非常强大。”
3.3 Arthas(阿里开源)
启动Arthas:
# 下载
curl -O https://arthas.aliyun.com/arthas-boot.jar
# 启动(选择进程)
java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.6.7
[INFO] Found existing java process, please choose one and input the serial number.
* [1]: 12345 com.example.Application
[2]: 23456 jar
1 # 输入1,attach到进程12345
核心功能:
功能1:dashboard(实时监控)
dashboard
# 实时显示:
线程:150个
内存:heap: 2.3GB/4GB, nonheap: 128MB
GC:YoungGC: 150次, FullGC: 5次
CPU:80%
功能2:thread(查看线程)
# 查看所有线程
thread
# 查看CPU最高的3个线程
thread -n 3
# 查看某个线程的栈
thread 123
# 查看死锁
thread -b
功能3:jad(反编译)
# 反编译类
jad com.example.service.UserService
# 输出:
public class UserService {
public void exportAllUsers() {
// 反编译后的代码
...
}
}
功能4:watch(方法监控)
# 监控方法执行
watch com.example.service.UserService exportAllUsers '{params, returnObj, throwExp}' -x 2
# 输出:
method=com.example.service.UserService.exportAllUsers
params=[]
returnObj=...
耗时:5234ms
功能5:trace(方法耗时分析)
trace com.example.service.UserService exportAllUsers
# 输出调用树:
`---ts=2024-10-31 15:30:45;thread_name=http-nio-8080-exec-1;id=1a;is_daemon=true;priority=5;
`---[5234.567ms] com.example.service.UserService:exportAllUsers()
+---[2.345ms] com.example.mapper.UserMapper:selectAll()
+---[5230.123ms] java.lang.StringBuilder:append() [500万次]
`---[2.099ms] java.lang.StringBuilder:toString()
# 发现:StringBuilder.append耗时5230ms(瓶颈)
阿西噶阿西:“Arthas能在线诊断,不用重启应用,非常方便。”
四、线上问题排查实战
4.1 CPU 100%排查
完整流程:
步骤:
# 1. top找进程
top
# PID 12345,CPU 400%
# 2. top -Hp找线程
top -Hp 12345
# 线程12346,CPU 99.8%
# 3. 转16进制
printf "%x\n" 12346
# 303a
# 4. jstack查看
jstack 12345 | grep 303a -A 30
# 找到UserService.exportAllUsers
# 5. 修复代码
# 优化字符串拼接,改用BufferedWriter
典型CPU高的原因:
1. 死循环
while(true) { ... }
2. 正则表达式回溯
复杂正则匹配大字符串
3. 频繁GC
Full GC耗CPU
4. 大量计算
复杂算法、加密解密
4.2 内存泄漏排查
完整流程:
graph TB
Problem[发现内存持续上升] --> Jstat[jstat监控老年代]
Jstat --> Growing{老年代<br/>持续增长?}
Growing -->|是| Dump1[第1次dump]
Dump1 --> Wait[等待1小时]
Wait --> Dump2[第2次dump]
Dump2 --> MAT[用MAT对比两次dump]
MAT --> Find[找到一直增长的对象]
Find --> Path[查看GC Roots路径]
Path --> Fix[修复代码]
Growing -->|否| Normal[正常,无泄漏]
对比分析:
第1次dump(14:00):
User对象:10万个,占10MB
第2次dump(15:00):
User对象:50万个,占50MB
结论:
1小时增长40万个User对象
→ User对象泄漏
4.3 死锁排查
完整流程:
# 1. jstack导出线程快照
jstack 12345 > thread.txt
# 2. 查找"deadlock"
grep -A 20 "deadlock" thread.txt
# 输出:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x7f8a1c004e00 (object 0xd5f20000),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x7f8a1c002e00 (object 0xd5f20010),
which is held by "Thread-1"
死锁代码示例:
Object lockA = new Object();
Object lockB = new Object();
// 线程1
new Thread(() -> {
synchronized (lockA) {
Thread.sleep(100);
synchronized (lockB) { // 等待lockB
doSomething();
}
}
}).start();
// 线程2
new Thread(() -> {
synchronized (lockB) {
Thread.sleep(100);
synchronized (lockA) { // 等待lockA
doSomething();
}
}
}).start();
// 结果:死锁
哈吉米:“jstack能自动检测死锁,太方便了。”
五、监控工具总结
5.1 核心要点
南北绿豆总结:
- jps:查看Java进程
- jstat:实时监控GC、类加载、编译
- jmap:dump堆内存、查看堆配置、对象统计
- jstack:线程快照、死锁检测、找CPU高的线程
- jinfo:查看和修改JVM参数
- VisualVM:可视化监控,图形化
- MAT:内存泄漏分析,自动生成报告
- Arthas:在线诊断,功能最全
5.2 面试高频问题
阿西噶阿西:
问题1:线上CPU 100%,如何排查?
排查步骤:
1. top找到Java进程PID
top
PID 12345, CPU 400%
2. top -Hp找到CPU高的线程
top -Hp 12345
线程12346, CPU 99.8%
3. 线程ID转16进制
printf "%x\n" 12346
输出:303a
4. jstack查看线程栈
jstack 12345 | grep 303a -A 30
找到具体方法
5. 定位代码,修复
常见原因:
- 死循环
- 正则回溯
- 频繁GC
问题2:线上OOM,如何排查?
排查步骤:
1. 配置自动dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=logs/heap.hprof
2. OOM时自动生成heap.hprof
3. 用MAT分析
- 打开heap.hprof
- 查看Leak Suspects(自动分析)
- 查看Histogram(对象统计)
- 查看Dominator Tree(占用最多的对象)
4. 找到泄漏对象
- 查看GC Roots路径
- 找到引用链
5. 修复代码
问题3:常用的JVM监控工具有哪些?
命令行工具:
- jps:查看Java进程
- jstat:实时监控(GC、内存)
- jmap:内存分析(dump、统计)
- jstack:线程分析(栈、死锁)
- jinfo:查看参数
可视化工具:
- VisualVM:JDK自带,监控 + 性能分析
- MAT:Eclipse出品,内存泄漏分析
- JProfiler:商业工具,功能全面
- Arthas:阿里开源,在线诊断
选择:
- 快速排查:Arthas
- 内存分析:MAT
- 综合监控:VisualVM
问题4:如何定位死锁?
步骤:
1. jstack导出线程快照
jstack <pid> > thread.txt
2. 查找deadlock关键字
grep "deadlock" thread.txt
3. 查看死锁线程
jstack会自动分析,显示:
- Thread-1持有锁A,等待锁B
- Thread-2持有锁B,等待锁A
4. 定位代码
根据线程栈,找到死锁的代码位置
5. 修复
- 统一加锁顺序
- 使用tryLock设置超时
- 减少锁的范围
问题5:Arthas有哪些常用命令?
监控类:
- dashboard:实时监控面板
- thread:线程分析
- jvm:JVM信息
诊断类:
- watch:监控方法调用
- trace:方法调用链路耗时
- stack:方法调用堆栈
- tt:方法调用时光隧道(录制回放)
查看类:
- jad:反编译类
- sc:查找类
- sm:查找方法
修改类:
- redefine:热更新类(不重启)
哈吉米:“掌握了这些工具,线上问题排查就有底气了。”


&spm=1001.2101.3001.5002&articleId=154196991&d=1&t=3&u=282d0b717cf54ad5aead2581c3eeccef)
4533

被折叠的 条评论
为什么被折叠?



