JVM 监控与诊断
JVM监控与诊断是Java应用运维和性能优化的核心技能。通过有效的监控,可以及时发现问题、定位瓶颈、优化性能。本章将介绍JVM提供的各种监控和诊断工具,以及常见问题的排查方法。
为什么需要JVM监控?
监控的作用
在生产环境中运行Java应用时,监控是必不可少的:
监控指标
JVM监控主要关注以下几类指标:
| 指标类型 | 具体内容 | 重要性 |
|---|---|---|
| 内存指标 | 堆内存使用、GC频率、GC时间 | 高 |
| CPU指标 | CPU使用率、线程数、线程状态 | 高 |
| 线程指标 | 线程数量、线程状态、死锁 | 高 |
| 类加载指标 | 加载类数量、卸载类数量 | 中 |
| 编译指标 | JIT编译次数、编译时间 | 中 |
命令行工具
JDK提供了丰富的命令行监控工具,这些工具是日常诊断的基础。
jps - 查看Java进程
jps(Java Virtual Machine Process Status Tool)用于列出正在运行的Java进程。
# 基本用法
jps
# 显示主类完整路径
jps -l
# 显示主类完整路径和JVM参数
jps -lv
# 显示主类完整路径、JVM参数和应用程序参数
jps -lvm
输出示例:
12345 org.example.MyApplication -Xmx2g -Denv=prod
12346 jdk.jcmd/sun.tools.jps.Jps -lvm
各列含义:
- 第一列:进程ID(PID)
- 第二列:主类名或JAR文件名
- 第三列及之后:传递给main方法的参数
jstat - JVM统计监控
jstat(Java Virtual Machine Statistics Monitoring Tool)用于监控JVM运行状态,特别是GC相关的统计信息。
常用选项
# 类加载统计
jstat -class <pid>
# 编译统计
jstat -compiler <pid>
# GC统计(最重要)
jstat -gc <pid>
# GC容量信息
jstat -gccapacity <pid>
# GC利用率(百分比形式,更易读)
jstat -gcutil <pid>
# GC原因
jstat -gccause <pid>
# 新生代详细统计
jstat -gcnew <pid>
# 老年代详细统计
jstat -gcold <pid>
# 元空间统计
jstat -gcmetacapacity <pid>
实时监控
# 每1秒输出一次,共输出10次
jstat -gcutil <pid> 1000 10
# 每5秒输出一次,无限循环
jstat -gcutil <pid> 5000
jstat -gc 输出详解
jstat -gc 12345
输出示例:
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
512.0 512.0 0.0 384.0 4096.0 2816.0 10240.0 6144.0 6400.0 5888.0 640.0 576.0 127 0.452 3 0.312 0.764
列含义说明:
| 列名 | 含义 | 单位 |
|---|---|---|
| S0C/S1C | Survivor 0/1区容量 | KB |
| S0U/S1U | Survivor 0/1区已使用 | KB |
| EC/EU | Eden区容量/已使用 | KB |
| OC/OU | 老年代容量/已使用 | KB |
| MC/MU | 元空间容量/已使用 | KB |
| CCSC/CCSU | 压缩类空间容量/已使用 | KB |
| YGC/YGCT | Young GC次数/总时间 | 次/秒 |
| FGC/FGCT | Full GC次数/总时间 | 次/秒 |
| GCT | GC总时间 | 秒 |
jstat -gcutil 输出详解
-gcutil以百分比形式输出,更直观:
jstat -gcutil 12345
输出示例:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 75.00 68.75 60.00 92.00 90.00 127 0.452 3 0.312 0.764
所有数值都是百分比,便于快速判断内存使用状况。
jmap - 内存映射工具
jmap(Memory Map)用于生成堆转储文件和查看内存分布。
常用命令
# 查看堆配置信息
jmap -heap <pid>
# 查看对象统计(按大小排序)
jmap -histo <pid> | head -30
# 查看存活对象统计(会触发Full GC)
jmap -histo:live <pid> | head -30
# 生成完整堆转储
jmap -dump:format=b,file=heap.hprof <pid>
# 只转储存活对象(会触发Full GC)
jmap -dump:live,format=b,file=heap_live.hprof <pid>
# 查看类加载器统计
jmap -clstats <pid>
# 查看等待finalize的对象
jmap -finalizerinfo <pid>
jmap -heap 输出解读
jmap -heap 12345
输出示例:
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 2147483648 (2048.0MB)
NewSize = 44564480 (42.5MB)
MaxNewSize = 715849728 (682.5MB)
OldSize = 89653248 (85.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.8MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
Heap Usage:
PS Young Generation
Eden Space:
capacity = 536870912 (512.0MB)
used = 369098752 (351.99999237060547MB)
free = 167772160 (160.0MB)
68.35937350551605% used
From Space:
capacity = 52428800 (50.0MB)
used = 0 (0.0MB)
free = 52428800 (50.0MB)
0.0% used
To Space:
capacity = 52428800 (50.0MB)
used = 0 (0.0MB)
free = 52428800 (50.0MB)
0.0% used
PS Old Generation
capacity = 1073741824 (1024.0MB)
used = 644245094 (614.4000015258789MB)
free = 429496730 (409.5999984741211MB)
60.0% used
jmap -histo 输出解读
jmap -histo 12345 | head -20
输出示例:
num #instances #bytes class name (module)
-------------------------------------------------------
1: 50000 4800000 java.lang.String
2: 30000 2400000 java.util.HashMap$Node
3: 20000 1600000 java.lang.Integer
4: 15000 1200000 byte[]
5: 10000 800000 java.util.HashMap
6: 8000 640000 java.lang.Object
7: 5000 400000 char[]
8: 3000 288000 java.lang.Class
重要列说明:
#instances:实例数量#bytes:占用字节数class name:类名
jstack - 线程堆栈工具
jstack(Stack Trace)用于获取线程堆栈信息,诊断线程问题。
常用命令
# 打印线程堆栈
jstack <pid>
# 打印额外锁信息
jstack -l <pid>
# 输出到文件
jstack <pid> > thread_dump.txt
# 强制打印(无响应时使用)
jstack -F <pid>
线程状态解读
线程堆栈中常见的线程状态:
| 状态 | 说明 | 可能原因 |
|---|---|---|
RUNNABLE | 运行中或等待CPU | 正常状态 |
BLOCKED | 阻塞等待锁 | 锁竞争激烈 |
WAITING | 无限等待 | Object.wait()、LockSupport.park() |
TIMED_WAITING | 限时等待 | Thread.sleep()、超时等待 |
NEW | 新创建未启动 | 线程未调用start() |
TERMINATED | 已终止 | 线程执行完成 |
死锁检测
jstack -l <pid> | grep -A 30 "Found one Java-level deadlock"
死锁输出示例:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f8b4c001000 (object 0x000000076b5c7d58, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x00007f8b4c002000 (object 0x000000076b5c7d60, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at DeadlockExample.method1(DeadlockExample.java:10)
- waiting to lock <0x000000076b5c7d58> (a java.lang.Object)
- locked <0x000000076b5c7d60> (a java.lang.Object)
at DeadlockExample.run(DeadlockExample.java:5)
"Thread-2":
at DeadlockExample.method2(DeadlockExample.java:20)
- waiting to lock <0x000000076b5c7d60> (a java.lang.Object)
- locked <0x000000076b5c7d58> (a java.lang.Object)
at DeadlockExample.run(DeadlockExample.java:15)
Found 1 deadlock.
jcmd - 多功能诊断工具
jcmd是JDK 7+推荐的多功能诊断工具,可以替代多个传统命令。
查看可用命令
# 查看指定进程支持的所有命令
jcmd <pid> help
常用命令
# JVM版本信息
jcmd <pid> VM.version
# JVM启动参数
jcmd <pid> VM.flags
jcmd <pid> VM.command_line
jcmd <pid> VM.system_properties
# 运行时信息
jcmd <pid> VM.info
# 线程信息
jcmd <pid> Thread.print
jcmd <pid> Thread.print -l # 包含锁信息
# 堆信息
jcmd <pid> GC.heap_info
jcmd <pid> GC.class_histogram
jcmd <pid> GC.heap_dump filename=heap.hprof
# 触发GC
jcmd <pid> GC.run
# 本地内存跟踪
jcmd <pid> VM.native_memory summary
jcmd <pid> VM.native_memory detail
# JIT编译器信息
jcmd <pid> Compiler.CodeHeap_Analytics
JFR录制控制
# 开始JFR录制
jcmd <pid> JFR.start name=test duration=60s filename=recording.jfr
# 查看正在进行的录制
jcmd <pid> JFR.check
# 转储录制数据
jcmd <pid> JFR.dump name=test filename=dump.jfr
# 停止录制
jcmd <pid> JFR.stop name=test
jinfo - 配置信息工具
jinfo用于查看和修改JVM参数。
# 查看所有JVM参数
jinfo <pid>
# 查看参数标志
jinfo -flags <pid>
# 查看特定参数
jinfo -flag MaxHeapSize <pid>
jinfo -flag UseG1GC <pid>
# 查看系统属性
jinfo -sysprops <pid>
# 动态修改参数(部分参数支持)
jinfo -flag +PrintGCDetails <pid>
jinfo -flag -PrintGCDetails <pid>
图形化工具
VisualVM
VisualVM是JDK自带的图形化监控工具,提供了直观的监控界面。
启动方式
# JDK 8及之前自带
jvisualvm
# JDK 9+需要单独下载
# 从 https://visualvm.github.io/ 下载
主要功能
插件安装
VisualVM支持丰富的插件扩展:
- 打开 工具 → 插件
- 选择需要的插件安装
- 常用插件:
- Visual GC:更详细的GC可视化
- BTrace Workbench:动态追踪
- JConsole Plugins:兼容JConsole插件
JConsole
JConsole是基于JMX的监控工具,适合监控JMX MBeans。
启动方式
jconsole
主要功能
- 内存监控:各内存区域的使用情况
- 线程监控:线程数量和状态
- 类监控:类加载情况
- MBeans:查看和管理JMX MBeans
JDK Mission Control (JMC)
JDK Mission Control是用于分析JFR数据的专业工具。
主要功能
- JFR录制数据分析
- 实时监控
- 堆分析
- JMX控制台
Java Flight Recorder (JFR)
JFR(Java Flight Recorder)是JDK内置的低开销事件记录框架,适合生产环境持续监控。
JFR概述
JFR的优势:
- 低开销:通常小于1%的性能影响
- 内置事件:涵盖JVM各个子系统
- 可扩展:支持自定义事件
- 生产就绪:适合长期运行的生产环境
启动JFR录制
方式一:命令行参数
# 启动时开始录制
java -XX:StartFlightRecording=duration=60s,filename=myrecording.jfr MyApp
# 完整参数示例
java -XX:StartFlightRecording=duration=5m,filename=recording.jfr,settings=profile MyApp
# 常用参数
# duration: 录制时长(s/m/h)
# filename: 输出文件名
# settings: 配置(default/profile)
# name: 录制名称
# maxsize: 最大文件大小
# maxage: 最大保留时间
方式二:jcmd命令
# 开始录制
jcmd <pid> JFR.start name=myrecording duration=60s filename=recording.jfr
# 带更多选项
jcmd <pid> JFR.start name=prod_recording settings=profile maxsize=100m maxage=1h
# 检查录制状态
jcmd <pid> JFR.check
# 转储录制数据
jcmd <pid> JFR.dump name=myrecording filename=dump.jfr
# 停止录制
jcmd <pid> JFR.stop name=myrecording
方式三:程序化控制
import jdk.jfr.*;
public class JfrExample {
public static void main(String[] args) {
// 获取Flight Recorder
FlightRecorder fr = FlightRecorder.getFlightRecorder();
// 创建配置
Configuration config = Configuration.getConfiguration("profile");
// 开始录制
Recording recording = new Recording(config);
recording.setName("MyRecording");
recording.start();
// 业务代码
doWork();
// 停止并保存
recording.stop();
recording.dump(Paths.get("recording.jfr"));
}
}
自定义JFR事件
import jdk.jfr.*;
// 定义自定义事件
@Label("Order Created")
@Description("订单创建事件")
public class OrderCreatedEvent extends Event {
@Label("Order ID")
public String orderId;
@Label("Customer ID")
public String customerId;
@Label("Amount")
public double amount;
}
// 使用自定义事件
public class OrderService {
public void createOrder(String orderId, String customerId, double amount) {
// 创建并提交事件
OrderCreatedEvent event = new OrderCreatedEvent();
event.orderId = orderId;
event.customerId = customerId;
event.amount = amount;
event.commit();
// 业务逻辑
// ...
}
}
JFR事件类型
JFR内置了大量事件类型:
| 事件类别 | 示例事件 |
|---|---|
| Java应用 | 类加载、线程启动、异常抛出 |
| 内存 | GC事件、对象分配、内存泄漏 |
| 代码执行 | 方法执行时间、编译事件 |
| 线程 | 锁竞争、线程等待、线程状态 |
| IO | 文件读写、网络通信 |
| 系统 | CPU使用、内存使用 |
使用JMC分析JFR
- 打开JDK Mission Control
- 文件 → 打开 → 选择.jfr文件
- 主要分析视图:
- 概览:整体概览
- 内存:GC统计、内存分配
- 代码:热点方法、编译信息
- 线程:线程状态、锁竞争
- IO:文件和网络操作
- 系统:CPU和内存使用
第三方工具
Arthas
Arthas是阿里巴巴开源的Java诊断工具,功能强大,适合线上问题排查。
安装启动
# 下载并启动
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 或使用快速安装脚本
curl -L https://arthas.aliyun.com/install.sh | sh
常用命令
监控命令:
# 查看Dashboard
dashboard
# 监控方法执行
watch com.example.UserService getUser '{params, returnObj, #cost}'
# 追踪方法调用路径和耗时
trace com.example.UserService getUser
# 监控方法调用统计
monitor -c 5 com.example.UserService getUser
# 方法调用参数和返回值
stack com.example.UserService getUser
诊断命令:
# 查看类信息
jad com.example.UserService
# 反编译代码
jad --source-only com.example.UserService getUser
# 查看类加载信息
classloader
# 查看JVM信息
jvm
# 查看线程信息
thread
thread -n 3 # 显示CPU使用率最高的3个线程
thread -b # 检测死锁
# 查看方法执行耗时
profiler start
# ... 业务执行 ...
profiler stop
热更新:
# 反编译类
jad --source-only com.example.UserService > UserService.java
# 编译(修改后)
mc /tmp/UserService.java -d /tmp
# 热更新
retransform /tmp/com/example/UserService.class
async-profiler
async-profiler是低开销的采样分析器,适合生产环境使用。
安装使用
# 下载
wget https://github.com/jvm-profiling-tools/async-profiler/releases/download/v2.9/async-profiler-2.9-linux-x64.tar.gz
tar -xzf async-profiler-2.9-linux-x64.tar.gz
# CPU分析
./profiler.sh -d 30 -f cpu.html <pid>
# 内存分配分析
./profiler.sh -d 30 -e alloc -f alloc.html <pid>
# 锁分析
./profiler.sh -d 30 -e lock -f lock.html <pid>
# Wall clock分析(包含等待时间)
./profiler.sh -d 30 -e wall -f wall.html <pid>
GCViewer
GCViewer用于可视化分析GC日志。
# 下载
wget https://github.com/chewiebug/GCViewer/releases/download/1.36/gcviewer-1.36.jar
# 运行
java -jar gcviewer-1.36.jar gc.log
常见诊断场景
场景一:内存泄漏诊断
现象:内存持续增长,最终OOM
诊断步骤:
# 1. 确认问题:观察老年代使用趋势
jstat -gcutil <pid> 5000 10
# 2. 多次生成堆转储对比
jmap -dump:live,format=b,file=heap1.hprof <pid>
# 等待一段时间后
jmap -dump:live,format=b,file=heap2.hprof <pid>
# 3. 使用MAT分析
# - 打开两个堆转储
# - 对比对象数量变化
# - 查看Dominator Tree
# - 分析Leak Suspects报告
MAT分析要点:
-
Shallow Heap vs Retained Heap:
- Shallow Heap:对象本身占用大小
- Retained Heap:对象及其引用链占用总大小
-
Dominator Tree:按Retained Heap排序,找到内存占用最大的对象
-
Path to GC Roots:查看对象为何无法被回收
场景二:CPU飙高诊断
现象:CPU使用率异常高
诊断步骤:
# 1. 找到高CPU的Java进程
top -H -p <pid>
# 2. 将线程ID转换为16进制
printf "%x\n" <thread_id>
# 3. 查看对应线程的堆栈
jstack <pid> | grep -A 30 <hex_thread_id>
# 4. 使用async-profiler分析
./profiler.sh -d 30 -f cpu.html <pid>
场景三:频繁Full GC诊断
现象:频繁Full GC,响应慢
诊断步骤:
# 1. 查看GC统计信息
jstat -gcutil <pid> 1000 20
# 2. 查看GC原因
jstat -gccause <pid> 1000
# 3. 分析GC日志
# 查找Full GC原因:System.gc()、元空间不足、晋升失败等
# 4. 检查元空间使用
jcmd <pid> GC.heap_info | grep -i metaspace
常见Full GC原因:
| 原因 | 症状 | 解决方案 |
|---|---|---|
| 老年代空间不足 | OU接近OC | 增大堆或优化对象生命周期 |
| 元空间不足 | MU接近MC | 增大元空间 |
| System.gc() | 代码显式调用 | 禁用:-XX:+DisableExplicitGC |
| 直接内存不足 | NIO相关 | 增大直接内存限制 |
场景四:线程死锁诊断
现象:程序挂起,无响应
诊断步骤:
# 方法一:jstack检测
jstack -l <pid> | grep -A 30 "Found one Java-level deadlock"
# 方法二:jcmd检测
jcmd <pid> Thread.print -l
# 方法三:VisualVM检测
# 打开Threads标签页 → 点击"Detect Deadlock"
# 方法四:Arthas检测
thread -b
场景五:类加载问题诊断
现象:ClassNotFoundException、NoClassDefFoundError
诊断步骤:
# 1. 查看类加载统计
jstat -class <pid>
# 2. 查看类加载详情
jcmd <pid> VM.classloaders
# 3. 使用Arthas查看类信息
jad com.example.MyClass
# 4. 查看类加载器层次
classloader -t
场景六:OOM快速诊断
现象:OutOfMemoryError
配置自动生成堆转储:
# 启动时配置
java -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heap.hprof \
-Xmx2g \
-jar app.jar
诊断步骤:
# 1. 确定OOM类型
# Java heap space: 堆内存不足
# Metaspace: 元空间不足
# GC overhead limit exceeded: GC时间过长
# Direct buffer memory: 直接内存不足
# 2. 使用MAT分析堆转储
# 3. 查看内存使用情况
jmap -heap <pid>
# 4. 对象直方图分析
jmap -histo:live <pid> | head -50
监控最佳实践
生产环境监控策略
关键监控指标和阈值建议
| 指标 | 健康阈值 | 警告阈值 | 危险阈值 |
|---|---|---|---|
| 老年代使用率 | < 70% | 70-85% | > 85% |
| Young GC频率 | < 1次/秒 | 1-5次/秒 | > 5次/秒 |
| Full GC频率 | < 1次/小时 | 1-4次/小时 | > 4次/小时 |
| GC停顿时间 | < 100ms | 100-500ms | > 500ms |
| 线程数量 | < 200 | 200-500 | > 500 |
| CPU使用率 | < 60% | 60-80% | > 80% |
监控工具选型建议
| 场景 | 推荐工具 | 说明 |
|---|---|---|
| 快速诊断 | jstat/jstack/jmap | 命令行,无需额外安装 |
| 深度分析 | VisualVM + MAT | 图形化,功能完整 |
| 生产监控 | JFR + Prometheus | 低开销,持续监控 |
| 线上诊断 | Arthas | 无需重启,热诊断 |
| 性能分析 | async-profiler | 低开销,火焰图 |
小结
JVM监控与诊断是保障Java应用稳定运行的关键能力:
- 命令行工具:jps、jstat、jmap、jstack、jcmd是日常诊断的基础
- 图形化工具:VisualVM、JConsole适合开发环境监控
- JFR:低开销事件记录,适合生产环境持续监控
- 第三方工具:Arthas、async-profiler提供更强大的诊断能力
- 常见场景:内存泄漏、CPU飙高、频繁GC、死锁等都有成熟的诊断流程
掌握这些工具和方法,能够快速定位和解决生产环境中的各种JVM问题。