🎛️ JFR与JMX:生产监控的"透视眼"!

54 阅读8分钟

面试考点:性能采样、事件记录、零开销诊断、监控指标

场景重现

老板:"生产环境又慢了!快查!" 😤
你:"我...我需要重启加参数..." 😰
老板:"啥?!重启生产?你疯了?!" 😡

如果你懂 JFRJMX,就能自信地说:
"不用重启,我现在就能看到所有指标!" 😎

🎯 什么是JFR和JMX?

一句话概括 💡

工具全称作用类比
JFRJava Flight Recorder飞行记录仪,记录JVM运行细节飞机黑匣子 🛫
JMXJava Management Extensions管理扩展,实时监控JVM状态汽车仪表盘 🚗

生活类比 🏥

JVM = 人体
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
JMX = 体检仪器 🩺
- 实时监控:心率、血压、体温
- 即时查看:现在的状态
- 轻量级:一直开着也没事

JFR = 24小时监护记录 📋
- 详细记录:过去24小时所有事件
- 事后分析:出问题了回溯查看
- 零开销:不影响正常活动
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

🛫 JFR(Java Flight Recorder)

什么是JFR?

JFR 是JVM内置的性能分析工具,最早是商业版特性(JDK 7),JDK 11开始免费开源!🎉

核心特性

✅ 零开销:对性能影响 < 1%
✅ 持续记录:可以一直开着
✅ 事件丰富:200+ 种事件类型
✅ 生产可用:不用重启,随时开关

JFR记录什么? 📝

事件分类

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔥 GC事件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- GC触发时间
- GC耗时
- 堆使用情况
- 晋升情况

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚙️ JIT编译事件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- 哪些方法被编译了
- 编译层级(C1/C2)
- 内联决策
- 逃逸分析结果

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💾 内存分配事件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- TLAB分配
- 对象大小
- 分配速率
- 分配位置(哪个类)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔒 锁竞争事件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- 锁等待时间
- 锁持有时间
- 锁竞争热点

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 线程事件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- 线程创建/销毁
- 线程状态变化
- CPU使用情况

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🌐 I/O事件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- 文件I/O
- 网络I/O
- Socket读写

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎯 方法执行事件(采样)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- CPU热点方法
- 方法调用栈
- 执行时间分布
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

如何使用JFR? 🛠️

方法1:启动时开启 🚀

# JDK 8
java -XX:+UnlockCommercialFeatures \
     -XX:+FlightRecorder \
     -XX:StartFlightRecording=duration=60s,filename=recording.jfr \
     -jar myapp.jar

# JDK 11+(免费了!)
java -XX:StartFlightRecording=duration=60s,filename=recording.jfr \
     -jar myapp.jar

参数说明

参数说明示例
duration录制时长60s, 10m, 2h
filename输出文件recording.jfr
settings配置模板profile, default
maxsize最大文件大小100M
maxage最长保留时间24h

方法2:运行时开启 ⚡(推荐)

# 1. 找到Java进程ID
jps

# 2. 开始录制(60秒)
jcmd <pid> JFR.start duration=60s filename=recording.jfr

# 3. 检查录制状态
jcmd <pid> JFR.check

# 4. 停止录制
jcmd <pid> JFR.stop

# 5. 导出录制(如果忘了设置filename)
jcmd <pid> JFR.dump filename=recording.jfr

实战示例

# 生产环境出问题,立即录制5分钟
jcmd 12345 JFR.start duration=5m filename=/tmp/problem.jfr settings=profile

# 输出:
Started recording 1. The result will be written to:
/tmp/problem.jfr

方法3:持续录制 🔄

# 启动持续录制(循环覆盖,保留最近1小时)
jcmd <pid> JFR.start name=continuous maxage=1h maxsize=500M

# 出问题时,立即导出
jcmd <pid> JFR.dump name=continuous filename=problem.jfr

生活类比

持续录制 = 行车记录仪 🚗
- 一直在录
- 只保留最近1小时
- 出事故了,立即保存录像

分析JFR文件 🔍

工具1:JDK Mission Control(JMC)🎛️

下载jdk.java.net/jmc/

启动

# 打开JMC
jmc

# 或直接打开文件
jmc -open recording.jfr

主要视图

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 概览(Overview)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- CPU使用率曲线
- 堆内存曲线
- GC次数和耗时
- 线程数变化

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔥 热点方法(Method Profiling)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Top 10最耗CPU的方法:
1. com.example.Service.process() - 35%
2. java.util.HashMap.get() - 12%
3. ...

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💾 内存(Memory)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- 分配最多的类
- TLAB分配统计
- 大对象分配

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔒 锁(Lock Instances)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- 锁竞争热点
- 等待时间最长的锁
- 锁持有时间

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🗑️ 垃圾回收(Garbage Collections)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- GC时间线
- 每次GC的详细信息
- 暂停时间分布
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

工具2:命令行分析 📟

# 打印JFR文件摘要
jfr print recording.jfr

# 输出为JSON格式
jfr print --json recording.jfr > result.json

# 只看GC事件
jfr print --events GCConfiguration,GarbageCollection recording.jfr

JFR配置模板 📋

两种预设模板

模板开销事件数量适用场景
default< 1%基础事件生产环境持续录制
profile~2%全部事件性能分析、问题排查

自定义模板

# 1. 导出默认模板
jfr configure --output custom.jfc

# 2. 编辑custom.jfc(XML文件)
# 修改事件采样频率等

# 3. 使用自定义模板
jcmd <pid> JFR.start settings=custom.jfc

🎛️ JMX(Java Management Extensions)

什么是JMX?

JMX 是Java的标准监控接口,提供实时的JVM指标。

特点

✅ 实时性:即时查看当前状态
✅ 标准化:所有JVM都支持
✅ 可远程:支持远程监控
✅ 可扩展:可以自定义MBean

MBean(管理Bean) 📦

MBean 是JMX的核心概念:

MBean = 可管理的Java对象
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
包含:
1. 属性(Attributes):可读/可写的值
2. 操作(Operations):可调用的方法
3. 通知(Notifications):事件通知
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

常用的MBean 📊

1. 内存(MemoryMXBean)💾

MemoryMXBean memoryMBean = ManagementFactory.getMemoryMXBean();

// 堆内存
MemoryUsage heapUsage = memoryMBean.getHeapMemoryUsage();
long used = heapUsage.getUsed();        // 已使用
long max = heapUsage.getMax();          // 最大值
long committed = heapUsage.getCommitted(); // 已分配

System.out.println("堆使用: " + used / 1024 / 1024 + " MB");
System.out.println("堆最大: " + max / 1024 / 1024 + " MB");

// 非堆内存(元空间)
MemoryUsage nonHeapUsage = memoryMBean.getNonHeapMemoryUsage();

2. 线程(ThreadMXBean)🧵

ThreadMXBean threadMBean = ManagementFactory.getThreadMXBean();

// 线程数
int threadCount = threadMBean.getThreadCount();
int peakThreadCount = threadMBean.getPeakThreadCount();

// 死锁检测
long[] deadlockedThreads = threadMBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
    System.out.println("发现死锁!线程ID: " + Arrays.toString(deadlockedThreads));
}

// 线程CPU时间
long threadCpuTime = threadMBean.getCurrentThreadCpuTime();

3. GC(GarbageCollectorMXBean)🗑️

List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();

for (GarbageCollectorMXBean gcBean : gcBeans) {
    String name = gcBean.getName();           // 例如:"G1 Young Generation"
    long count = gcBean.getCollectionCount(); // GC次数
    long time = gcBean.getCollectionTime();   // GC总耗时(毫秒)
    
    System.out.println(name + ": " + count + "次, " + time + "ms");
}

4. 类加载(ClassLoadingMXBean)📚

ClassLoadingMXBean classLoadingMBean = ManagementFactory.getClassLoadingMXBean();

int loadedClassCount = classLoadingMBean.getLoadedClassCount();
long totalLoadedClassCount = classLoadingMBean.getTotalLoadedClassCount();
long unloadedClassCount = classLoadingMBean.getUnloadedClassCount();

System.out.println("当前加载类: " + loadedClassCount);
System.out.println("总共加载类: " + totalLoadedClassCount);
System.out.println("卸载类: " + unloadedClassCount);

5. 运行时(RuntimeMXBean)⏱️

RuntimeMXBean runtimeMBean = ManagementFactory.getRuntimeMXBean();

String name = runtimeMBean.getName();         // 进程ID@主机名
long uptime = runtimeMBean.getUptime();       // 运行时长(毫秒)
List<String> args = runtimeMBean.getInputArguments(); // JVM参数

System.out.println("进程: " + name);
System.out.println("运行时长: " + uptime / 1000 + "秒");
System.out.println("JVM参数: " + args);

远程JMX 🌐

开启远程JMX

java -Dcom.sun.management.jmxremote \
     -Dcom.sun.management.jmxremote.port=9999 \
     -Dcom.sun.management.jmxremote.authenticate=false \
     -Dcom.sun.management.jmxremote.ssl=false \
     -jar myapp.jar

⚠️ 生产环境要开启认证!

# 开启认证和SSL
java -Dcom.sun.management.jmxremote \
     -Dcom.sun.management.jmxremote.port=9999 \
     -Dcom.sun.management.jmxremote.authenticate=true \
     -Dcom.sun.management.jmxremote.ssl=true \
     -Dcom.sun.management.jmxremote.password.file=/path/to/jmxremote.password \
     -Dcom.sun.management.jmxremote.access.file=/path/to/jmxremote.access \
     -jar myapp.jar

连接远程JMX

# 使用JConsole
jconsole <host>:9999

# 使用VisualVM
jvisualvm
# 然后:File → Add JMX Connection → <host>:9999

自定义MBean 🔧

场景:监控自己的业务指标

// 1. 定义MBean接口(必须以MBean结尾)
public interface BusinessMetricsMBean {
    // 属性(Getter/Setter)
    long getRequestCount();
    double getAverageResponseTime();
    
    // 操作
    void reset();
}

// 2. 实现MBean
public class BusinessMetrics implements BusinessMetricsMBean {
    private AtomicLong requestCount = new AtomicLong(0);
    private AtomicLong totalResponseTime = new AtomicLong(0);
    
    public void recordRequest(long responseTime) {
        requestCount.incrementAndGet();
        totalResponseTime.addAndGet(responseTime);
    }
    
    @Override
    public long getRequestCount() {
        return requestCount.get();
    }
    
    @Override
    public double getAverageResponseTime() {
        long count = requestCount.get();
        return count == 0 ? 0 : (double) totalResponseTime.get() / count;
    }
    
    @Override
    public void reset() {
        requestCount.set(0);
        totalResponseTime.set(0);
    }
}

// 3. 注册MBean
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName name = new ObjectName("com.example:type=BusinessMetrics");
BusinessMetrics mbean = new BusinessMetrics();
mbs.registerMBean(mbean, name);

// 4. 在业务代码中使用
@GetMapping("/api/users")
public List<User> getUsers() {
    long start = System.currentTimeMillis();
    List<User> users = userService.findAll();
    long responseTime = System.currentTimeMillis() - start;
    
    // 记录指标
    mbean.recordRequest(responseTime);
    
    return users;
}

现在可以在JConsole中看到自定义指标了!🎉

🎯 实战案例

案例1:定位CPU飙升 🔥

问题:生产环境CPU突然飙到100%

步骤

# 1. 立即开始JFR录制(profile模式)
jcmd <pid> JFR.start duration=60s filename=cpu-spike.jfr settings=profile

# 2. 等待录制完成

# 3. 下载jfr文件到本地

# 4. 用JMC打开
jmc -open cpu-spike.jfr

# 5. 查看"Method Profiling"
# 发现:com.example.Service.heavyMethod() 占用95% CPU

# 6. 优化代码

发现

// 问题代码:
public void heavyMethod() {
    while (true) {  // 😱 无限循环!
        // ...
    }
}

案例2:内存泄漏排查 💧

问题:堆内存持续增长,最终OOM

步骤

# 1. 持续录制JFR
jcmd <pid> JFR.start name=leak maxage=1h

# 2. 等待几小时,观察内存

# 3. 内存涨到90%时,导出JFR
jcmd <pid> JFR.dump name=leak filename=leak.jfr

# 4. 用JMC分析
# 查看"Memory" → "Allocations"
# 发现:com.example.Cache 分配了10GB内存!

# 5. 抓取堆转储
jmap -dump:live,format=b,file=heap.hprof <pid>

# 6. 用MAT分析,确认泄漏点

案例3:锁竞争优化 🔒

问题:接口响应慢

步骤

# 1. JFR录制
jcmd <pid> JFR.start duration=2m filename=lock.jfr settings=profile

# 2. JMC分析
# 查看"Lock Instances"
# 发现:synchronized (this) 锁等待时间占50%

# 3. 优化代码

优化

// 优化前:粗粒度锁
public synchronized void process() {
    doA();  // 10ms
    doB();  // 100ms
    doC();  // 10ms
}

// 优化后:细粒度锁
public void process() {
    synchronized (lockA) { doA(); }
    doB();  // 不需要锁
    synchronized (lockC) { doC(); }
}

🎛️ JMX监控大屏 📊

使用Prometheus + Grafana

1. 暴露JMX指标

<!-- 添加JMX Exporter -->
<dependency>
    <groupId>io.prometheus.jmx</groupId>
    <artifactId>jmx_prometheus_javaagent</artifactId>
    <version>0.18.0</version>
</dependency>
# 启动时加载jmx_exporter
java -javaagent:jmx_prometheus_javaagent.jar=8080:config.yaml \
     -jar myapp.jar

config.yaml

rules:
  - pattern: "java.lang<type=Memory><HeapMemoryUsage>used"
    name: jvm_heap_used_bytes
  - pattern: "java.lang<type=GarbageCollector, name=(.*)><>CollectionCount"
    name: jvm_gc_count
    labels:
      gc: "$1"

2. Prometheus采集

# prometheus.yml
scrape_configs:
  - job_name: 'java-app'
    static_configs:
      - targets: ['localhost:8080']

3. Grafana展示

导入JVM监控Dashboard(ID: 4701)

效果

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 JVM监控大屏
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
| 堆内存使用  [====60%====     ] 4.8G/8G |
| GC次数     Minor: 1250  Full: 2       |
| 线程数      [======45======]           |
| CPU使用率   [===30%===]                |
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

💡 Pro Tips

Tip 1: JFR生产环境最佳实践 🏭

# 1. 持续录制(循环缓冲,保留最近1小时)
jcmd <pid> JFR.start name=continuous \
                     settings=default \
                     maxage=1h \
                     maxsize=500M

# 2. 出问题时立即导出
jcmd <pid> JFR.dump name=continuous filename=/tmp/problem-$(date +%s).jfr

# 3. 不影响性能(开销 < 1%)

Tip 2: JMX监控告警 🚨

// 设置内存告警
MemoryMXBean memoryMBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryMBean.getHeapMemoryUsage();

double usagePercent = (double) heapUsage.getUsed() / heapUsage.getMax() * 100;

if (usagePercent > 90) {
    // 发送告警
    alertService.send("堆内存使用率超过90%!");
}

Tip 3: JFR + JMX组合拳 🥊

实时监控 → JMX
问题回溯 → JFR
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. JMX发现CPU高
2. 立即启动JFR录制
3. 录制完成后分析热点
4. 定位问题代码
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

🎓 面试要点

高频问题

Q1: JFR和JProfiler/YourKit有什么区别?

答案

特性JFR商业工具
开销< 1%5-10%
生产可用✅ 是⚠️ 有风险
价格免费昂贵
功能全面更丰富

Q2: 如何零停机开启JFR?

答案

# 不需要重启,直接开启
jcmd <pid> JFR.start duration=60s filename=recording.jfr

Q3: JMX的性能开销是多少?

答案

  • 开启JMX:几乎无开销(< 0.1%)
  • 频繁查询:取决于查询频率
  • 建议:每1-5秒采集一次指标

🎉 总结

🎯 核心特点

工具优势适用场景
JFR零开销、详细记录性能分析、问题排查
JMX实时监控、标准化日常监控、健康检查

📋 使用建议

日常监控 → JMX + Prometheus + Grafana
问题排查 → JFR + JMC
持续优化 → 两者结合

记住:JFR和JMX是生产环境的"透视眼",掌握它们,你就能运筹帷幄!🎯

🌟 "好的监控体系 = 问题发现 + 快速定位 + 精准优化" 😎