跳到主要内容

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/S1CSurvivor 0/1区容量KB
S0U/S1USurvivor 0/1区已使用KB
EC/EUEden区容量/已使用KB
OC/OU老年代容量/已使用KB
MC/MU元空间容量/已使用KB
CCSC/CCSU压缩类空间容量/已使用KB
YGC/YGCTYoung GC次数/总时间次/秒
FGC/FGCTFull GC次数/总时间次/秒
GCTGC总时间

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支持丰富的插件扩展:

  1. 打开 工具 → 插件
  2. 选择需要的插件安装
  3. 常用插件:
    • 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

  1. 打开JDK Mission Control
  2. 文件 → 打开 → 选择.jfr文件
  3. 主要分析视图:
    • 概览:整体概览
    • 内存: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分析要点

  1. Shallow Heap vs Retained Heap

    • Shallow Heap:对象本身占用大小
    • Retained Heap:对象及其引用链占用总大小
  2. Dominator Tree:按Retained Heap排序,找到内存占用最大的对象

  3. 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停顿时间< 100ms100-500ms> 500ms
线程数量< 200200-500> 500
CPU使用率< 60%60-80%> 80%

监控工具选型建议

场景推荐工具说明
快速诊断jstat/jstack/jmap命令行,无需额外安装
深度分析VisualVM + MAT图形化,功能完整
生产监控JFR + Prometheus低开销,持续监控
线上诊断Arthas无需重启,热诊断
性能分析async-profiler低开销,火焰图

小结

JVM监控与诊断是保障Java应用稳定运行的关键能力:

  1. 命令行工具:jps、jstat、jmap、jstack、jcmd是日常诊断的基础
  2. 图形化工具:VisualVM、JConsole适合开发环境监控
  3. JFR:低开销事件记录,适合生产环境持续监控
  4. 第三方工具:Arthas、async-profiler提供更强大的诊断能力
  5. 常见场景:内存泄漏、CPU飙高、频繁GC、死锁等都有成熟的诊断流程

掌握这些工具和方法,能够快速定位和解决生产环境中的各种JVM问题。

参考资料