06.2 - 常用工具(jps / jstat / jmap / jstack / jinfo)
定位: JDK自带的5个命令行诊断工具——每个生产服务器上都有,不需要额外安装面试高频度: ⭐⭐⭐⭐⭐
一、为什么是这五个工具?
生产服务器通常没有GUI,不能装Arthas,不能装MAT。但只要装了JDK,这五个工具就可用。它们是JVM诊断的最后一道防线。
诊断场景 → 工具映射:
不知道有哪些Java进程 → jps
怀疑GC有问题 → jstat
想看内存分布 → jmap
怀疑线程卡住 → jstack
想查JVM参数 → jinfo二、诊断工作流全景
┌─────────────────┐
│ 系统出问题了 │
└────────┬────────┘
│
┌────────▼────────┐
│ jps -lvm │
│ 找到目标PID │
└────────┬────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ jstat -gc │ │ top -H -p │ │ jstack │
│ GC是否异常? │ │ CPU最高的线程 │ │ 线程状态检查 │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ jmap -heap │ │ jstack + TID │ │ jstack -l │
│ 内存总览 │ │ 定位代码 │ │ 检查死锁 │
└──────┬───────┘ └──────────────┘ └──────────────┘
│
┌──────▼───────┐
│ jmap -dump │
│ heap dump │
│ → MAT分析 │
└──────────────┘三、jps — JVM Process Status
WHY: 所有JVM诊断的第一步——你需要PID。
常用命令
# 基础用法 - 显示PID和主类名
jps -l
# 输出示例:
# 12345 com.example.MyApplication
# 23456 org.apache.catalina.startup.Bootstrap
# 最常用组合 - 完整信息
jps -lvm
# 输出示例:
# 12345 com.example.MyApplication -Xms2g -Xmx2g -Dspring.profiles.active=prod
# 23456 org.apache.catalina.startup.Bootstrap -Dcatalina.base=/opt/tomcat| 参数 | 含义 | 说明 |
|---|---|---|
-l | 显示完整主类名 | 默认只显示简单类名 |
-v | 显示JVM参数 | 诊断参数配置 |
-m | 显示main方法参数 | 启动参数 |
-V | 不显示JVM参数 | 精简输出 |
实战技巧
jps会连接JVM的/tmp/hsperfdata_<user>目录。如果进程重启后该目录没有更新,可能看到僵尸进程。遇到这种情况可以用ps aux | grep java替代。
四、jstat — JVM Statistics Monitoring
WHY: 最重要的监控工具。实时查看GC状态、内存使用率、类加载情况。随时检查GC是否异常。
4.1 jstat -gc — GC统计(最详细)
jstat -gc <pid> 1000 3 # 每1秒采样,共3次输出示例(带注释):
S0C S1C S0U S1U EC EU OC OU MC MU YGC YGCT FGC FGCT GCT
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ← 不存在的行(示例结构)
512.0 512.0 120.5 0.0 4608.0 2048.3 10240.0 5120.5 -- -- -- -- -- -- --各列含义(必背):
| 列 | 含义 | 诊断意义 |
|---|---|---|
| S0C | Survivor 0 容量(KB) | 看Survivor是否过小 |
| S1C | Survivor 1 容量(KB) | 与S0C对称 |
| S0U | Survivor 0 使用量(KB) | 活跃对象占Survivor比例 |
| S1U | Survivor 1 使用量(KB) | 谁是非空的Survivor |
| EC | Eden容量(KB) | 新生代分配 |
| EU | Eden使用量(KB) | 当前Eden占用 |
| OC | Old容量(KB) | 老年代总大小 |
| OU | Old使用量(KB) | 重点指标—OU持续上涨说明可能有泄漏 |
| MC | Metaspace容量(KB) | 元空间已提交大小 |
| MU | Metaspace使用量(KB) | MU接近MC说明需扩容 |
| YGC | Young GC次数 | Minor GC频率 |
| YGCT | Young GC总耗时(秒) | 每次YGC时间=YGCT/YGC |
| FGC | Full GC次数 | 重点指标—快速增长说明有问题 |
| FGCT | Full GC总耗时(秒) | 每次FGC时间=FGCT/FGC |
| GCT | GC总耗时(秒) | YGCT+FGCT |
4.2 jstat -gcutil — GC使用率百分比(最直观)
jstat -gcutil <pid> 1000输出示例:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
23.52 0.00 44.50 50.05 85.30 82.10 1256 4.567 3 0.345 4.912- S0=23.52%: Survivor 0 使用了 23.52%
- E=44.50%: Eden 使用了 44.50%
- O=50.05%: Old 使用了 50.05% ✅ 最需要关注的指标
- M=85.30%: Metaspace 使用了 85.30%
- YGC=1256: 发生了1256次Young GC
- FGC=3: 发生了3次Full GC
当你在一台服务器上且只需要"GC是否正常"的快速判断时,
jstat -gcutil是最好的选择。
4.3 jstat -gccause — 最近GC原因
jstat -gccause <pid> 1000输出示例:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT LGCC GCC
12.5 0.00 30.0 60.0 80.0 75.0 2000 8.000 10 1.200 9.200 Allocation Failure System.gc()- LGCC: 上次GC原因 (Last GC Cause)
- GCC: 当前GC原因 (Current GC Cause)
如果LGCC显示
System.gc(),说明有代码显式调用了System.gc()。此时应加-XX:+DisableExplicitGC。
4.4 其他jstat命令
jstat -gccapacity <pid> # 各代容量信息
jstat -gcnew <pid> # 新生代详细信息
jstat -gcold <pid> # 老年代详细信息
jstat -gcmetacapacity <pid> # 元空间容量
jstat -class <pid> # 类加载统计(看加载/卸载数量)
jstat -compiler <pid> # JIT编译统计实战:快速GC健康检查
# 检查Full GC是否异常
jstat -gcutil <pid> 1000 5
# 正常输出特征: FGC数不增长或缓慢增长
# 异常输出特征: FGC每几秒增长1次
# 检查老年代使用率趋势
OU=$(jstat -gc <pid> | awk '{print $16}') # 不断上涨说明泄漏五、jmap — Memory Map
WHY: 查看堆内存分布,dump堆用于深度分析。
5.1 堆信息
# 堆概要(安全,不会触发GC)
jmap -heap <pid>输出示例:
Attaching to process ID 12345, please wait...
Debugger attached successfully.
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 2147483648 (2048.0MB)
NewSize = 734003200 (700.0MB)
MaxNewSize = 734003200 (700.0MB)
OldSize = 1413480448 (1348.0MB)
Heap Usage:
New Generation (Eden + 1 Survivor):
capacity = 661127168 (630.5MB)
used = 330285056 (315.0MB)
free = 330842112 (315.5MB)
50.0% used
Eden Space:
capacity = 587202560 (560.0MB)
used = 293601280 (280.0MB)
free = 293601280 (280.0MB)
50.0% used
From Space:
capacity = 73924608 (70.5MB)
used = 36686848 (35.0MB)
free = 37237760 (35.5MB)
49.7% used
Tenured Generation:
capacity = 1413480448 (1348.0MB)
used = 1137734734 (1085.0MB)
free = 275745714 (263.0MB)
80.5% used ⚠️ 老年代使用率偏高!5.2 类直方图
# 安全模式 - 不触发Full GC
jmap -histo <pid>
# ⚠️ 生产慎用模式 - 先触发Full GC再统计
jmap -histo:live <pid>输出示例(截取前5行):
num #instances #bytes class name
----------------------------------------------
1: 50000 200000000 [B ← byte[] 最大
2: 120000 19200000 java.util.HashMap$Node
3: 80000 6400000 java.lang.String
4: 100000 3200000 java.lang.Object[]⚠️
:live陷阱:jmap -histo:live会触发Full GC!在生产环境高峰期使用会引发STW。不加:live是安全的,只扫描已有数据。jmap -dump同理会触发Full GC。
5.3 堆Dump
# ⚠️ 触发Full GC!生产慎用!
jmap -dump:format=b,file=/tmp/heap.hprof <pid>
# 不触发Full GC的方式(Linux)
gcore <pid> # 先core dump
jmap -dump:format=b,file=heap.hprof /usr/lib/jvm/java-8-openjdk-amd64/lib/amd64/server/libjvm.so <core_file>正确做法: 在启动参数中预配置自动dump,而不是出事后再jmap -dump:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump/heap-$(date).hprof六、jstack — Stack Trace
WHY: 查看线程状态,检测死锁,定位卡住的线程。
6.1 基础用法
# 完整线程dump
jstack <pid>
# 包含锁信息
jstack -l <pid>
# 混合模式(包含C++帧)
jstack -m <pid>6.2 线程状态解读
jstack输出中每个线程的状态:
| 状态 | 含义 | 排查方向 |
|---|---|---|
| RUNNABLE | 正在执行或等待CPU | 正常 ✅ |
| BLOCKED | 等待获取锁(被其他线程持有) | 🔴 锁竞争热点 |
| WAITING | Object.wait() 无超时 | 线程池正常等待任务 |
| TIMED_WAITING | sleep/wait(timeout)/parkNanos | 线程池正常等待 |
| TERMINATED | 线程已结束 | 检查是否本应存活 |
6.3 线程dump示例(标注版)
"http-nio-8080-exec-18" #85 daemon prio=5 os_prio=0 tid=0x00007f3c900b8000 nid=0x2b17
waiting for monitor entry [0x00007f3c8e5f4000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.service.OrderService.createOrder(OrderService.java:125)
- waiting to lock <0x000000076b5c6a60> (a java.lang.Object) ← 等待这个锁
at com.example.controller.OrderController.create(OrderController.java:45)
- locked <0x000000076b5c6a58> (a java.lang.Object) ← 持有了另一个锁
"http-nio-8080-exec-17" #84 daemon prio=5 os_prio=0 tid=0x00007f3c900b6800 nid=0x2b16
waiting for monitor entry [0x00007f3c8e6f5000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.service.InventoryService.deduct(InventoryService.java:78)
- waiting to lock <0x000000076b5c6a58> (a java.lang.Object) ← 等待锁A
at com.example.controller.InventoryController.deduct(InventoryController.java:22)
- locked <0x000000076b5c6a60> (a java.lang.Object) ← 持有锁B上面这组dump表明"exec-18持有锁A等待锁B,exec-17持有锁B等待锁A"——典型的死锁。
6.4 死锁检测
jstack -l <pid>jstack会自动检测死锁并输出:
Found one Java-level deadlock:
=============================
"http-nio-8080-exec-18":
waiting to lock monitor 0x00007f3c8e5f4000 (object 0x000000076b5c6a58, a java.lang.Object),
which is held by "http-nio-8080-exec-17"
"http-nio-8080-exec-17":
waiting to lock monitor 0x00007f3c8e6f5000 (object 0x000000076b5c6a60, a java.lang.Object),
which is held by "http-nio-8080-exec-18"
Found 1 deadlock.🔑 jstack会主动扫描线程间锁的依赖关系,如果形成环,就报告死锁。这是排查死锁最快的方式,没有之一。
6.5 排查CPU飙升的jstack用法
# Step 1: 找到CPU最高的线程TID
top -H -p <pid>
# Step 2: 将TID转为十六进制
printf '%x\n' <tid>
# 输出: a2c
# Step 3: 在jstack中搜索
jstack <pid> | grep -A 30 '0xa2c'详细方法论见 **CPU飙升排查方法论**。
七、jinfo — Configuration Info
WHY: 查看运行时JVM参数,甚至动态修改部分参数,不需要重启进程。
7.1 查看参数
# 查看所有非默认参数
jinfo -flags <pid>
# 查看单个参数
jinfo -flag MaxHeapSize <pid>
# 输出: -XX:MaxHeapSize=2147483648
# 查看是否开启
jinfo -flag PrintGCDetails <pid>
# 输出: -XX:+PrintGCDetails7.2 动态修改参数
# 开启GC日志(无需重启!)
jinfo -flag +PrintGCDetails <pid>
jinfo -flag +PrintGCDateStamps <pid>
jinfo -flag +PrintGCApplicationStoppedTime <pid>
# 修改参数值
jinfo -flag MaxMetaspaceSize=256m <pid>⚠️ 并非所有参数都可动态修改。只有标记为
manageable的参数才支持运行时修改。检查方式:bashjava -XX:+PrintFlagsFinal -version | grep manageable常见的manageable参数:
PrintGCDetails,HeapDumpOnOutOfMemoryError,MaxMetaspaceSize,MaxHeapFreeRatio,MinHeapFreeRatio。
八、综合实战:一次完整的排查过程
场景:服务响应变慢
# Step 1: 找到PID
jps -lvm | grep my-app
# 输出: 12345 /opt/app/my-app.jar -Xms2g -Xmx2g
# Step 2: 检查GC情况
jstat -gcutil 12345 1000 5
# S0 S1 E O M YGC YGCT FGC FGCT GCT
# -- -- -- -- -- -- -- -- -- --
# 0.0 0.0 80.0 95.0 85.0 5000 30.0 50 25.0 55.0
# ^^^^
# FGC=50且每次FGCT=0.5秒! 这说明Full GC频繁
# Step 3: 看堆内存分布
jamp -heap 12345
# 看到Old使用率95%,说明堆快满了
# Step 4: 排查内存泄漏嫌疑对象
jmap -histo 12345 | head -20
# 发现 byte[] 和 HashMap$Node 占用巨大
# Step 5: 如果怀疑线程卡住,检查线程状态
jstack -l 12345 | grep "java.lang.Thread.State" | sort | uniq -c
# 输出:
# 42 RUNNABLE
# 55 WAITING (on object monitor)
# 3 BLOCKED (on object monitor) ← 少数阻塞,正常九、面试视角
高频面试题
| 面试题 | 考察点 | 回答要点 |
|---|---|---|
| "jstat的S0C、S1C、EC、OC是什么意思?" | 工具熟练度 | 分别是Survivor0/Survivor1/Eden/Old的容量(KB) |
| "jmap dump会触发Full GC吗?" | 生产安全意识 | 会。:live形式和dump都触发Full GC,生产需谨慎 |
| "jstack如何检测死锁?" | 原理理解 | 扫描锁依赖图,发现循环等待环则报告 |
| "jinfo的-flag +和-flag -有什么区别?" | 动态调优 | +开启布尔参数,-关闭 |
| "jps -lvm分别代表什么?" | 基础 | -l完整类名,-vJVM参数,-mmain参数 |
| "jstat中OU一直上涨但FGC没有变化说明什么?" | 问题分析 | 老年代使用量在增加但还没触发Full GC(可能还有空间,或未达到GC阈值),需要关注趋势 |
| "jstack中BLOCKED和WAITING有什么区别?" | Java并发基础 | BLOCKED是等待获取锁(进入synchronized块),WAITING是调用wait/join/park后主动等待通知 |
面试回答模板
Q: 线上CPU100%怎么排查? A: 四步走:
top找PID →top -H -p <pid>找TID →printf '%x'转hex →jstack pid | grep -A 30定位。更快的方案是用Arthas的thread -n 3。
Q: 排查过的JVM问题中最难的一次? A: (STAR法则) 某次服务OOM,
jstat发现Old涨得快,jmap -histo发现byte[]巨大,jstack检查线程发现有大量未关闭的Socket连接,最终定位是网络库的连接池泄漏——连接对象不被GC回收因为线程池ThreadLocal持有引用。
📚 相关链接
- ← 返回 **调优索引**
- **CPU飙升排查方法论** - 使用jstack排查CPU问题
- **OOM排查方法论** - 使用jmap和dump分析OOM
- **FullGC频繁排查方法论** - 使用jstat诊断Full GC
- **图形化工具(JVisualVM与Arthas)** - 更强大的工具
- **Java堆** - 堆内存结构
- **对象创建过程** - 对象分配基础