面试考点:性能采样、事件记录、零开销诊断、监控指标
场景重现:
老板:"生产环境又慢了!快查!" 😤
你:"我...我需要重启加参数..." 😰
老板:"啥?!重启生产?你疯了?!" 😡
如果你懂 JFR 和 JMX,就能自信地说:
"不用重启,我现在就能看到所有指标!" 😎
🎯 什么是JFR和JMX?
一句话概括 💡
| 工具 | 全称 | 作用 | 类比 |
|---|---|---|---|
| JFR | Java Flight Recorder | 飞行记录仪,记录JVM运行细节 | 飞机黑匣子 🛫 |
| JMX | Java 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)🎛️
启动:
# 打开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是生产环境的"透视眼",掌握它们,你就能运筹帷幄!🎯
🌟 "好的监控体系 = 问题发现 + 快速定位 + 精准优化" 😎