GC 停顿、内存泄漏、接口响应变慢------线上服务出问题的时候,你是否也曾对着监控面板发呆,不知道根因在哪?
今天聊聊 Java 自带的性能诊断神器 JFR(Java Flight Recorder),配合 Spring Boot 使用,效果翻倍。
JFR 是什么
JFR 是 JDK 内置的性能采集工具,知道的人不多,但用过的人都说是「真香」。
几个核心优势:
- 开销可控:通过事件开关控制采集粒度,精准场景下开销可压到极低
- 事件丰富:GC、线程、IO、锁、CPU、异常......100+ 种事件类型
- 历史回溯:录制文件可以离线分析,事后定位没问题
- 持续录制:支持后台常驻,出问题随时有数据兜底
JDK 8 及之前 JFR 是商业特性,JDK 9+ 免费开源。
生产环境启用前建议确认:磁盘空间充足、开启了 JFR 权限控制、采集的事件范围符合需求。
Spring Boot 启用 JFR
方式一:启动参数(最简单)
bash
java -XX:StartFlightRecording:filename=recording.jfr,duration=60s -jar app.jar
更多参数配置:
bash
java -XX:StartFlightRecording=\
filename=app-recording.jfr,\
dumponexit=true,\
maxsize=500M,\
maxage=1d,\
settings=profile -jar app.jar
参数说明:
| 参数 | 含义 |
|---|---|
filename |
录制文件保存路径 |
dumponexit |
JVM 退出时自动 dump |
maxsize |
单文件最大 size |
maxage |
最老数据的保留时间 |
settings |
模板(production/profile) |
方式二:API 动态控制
Spring Boot 可以通过 JMX 远程控制 JFR 开始/停止:
java
@Service
public class JfrController {
private final MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
private static final String JFR_BEAN_NAME = "com.oracle.jdk:jfrType=FlightRecorder";
@PostMapping("/jfr/start")
public String start(@RequestParam String filename) throws Exception {
ObjectName name = new ObjectName(JFR_BEAN_NAME);
Map<String, String> settings = Map.of(
"jdk.JavaMonitorEnter#enabled", "true",
"jdk.SocketRead#enabled", "true"
);
mBeanServer.invoke(name, "startRecording",
new Object[]{filename, settings},
new String[]{String.class.getName(), Map.class.getName()}
);
return "started: " + filename;
}
@PostMapping("/jfr/stop")
public String stop() throws Exception {
ObjectName name = new ObjectName(JFR_BEAN_NAME);
mBeanServer.invoke(name, "stopRecording", new Object[]{}, new String[]{});
return "stopped";
}
}
方式三:Actuator 集成
Spring Boot Actuator 也能暴露 JFR 端点:
yaml
management:
endpoints:
web:
exposure:
include: health,info,jfr
jfr:
enabled: true
directory: /var/log/jfr
录制文件怎么分析
JDK Mission Control (JMC)
这是官方提供的 GUI 分析工具,JDK 11+ 自带。
bash
jmc
打开录制文件后,重点看这几个视图:
1. Recording View - 事件列表和分布
2. Code Path - 热点方法调用链
3. Memory - GC 和对象分配
4. Threads - 锁竞争、线程状态
5. I/O - 文件和网络延迟
命令行快速分析
bash
# 概览
jfr summary recording.jfr
# 只看某类事件
jfr print --events "jdk.GC*,jdk.JavaMonitorEnter" recording.jfr
# 找出耗时最长的调用
jfr print --events "jdk.ExecutionSample" recording.jfr | head -30
代码分析脚本
写个脚本批量处理多个录制文件:
java
public class JfrAnalyzer {
public static void main(String[] args) throws Exception {
var file = new File(args[0]);
Map<String, Long> durations = new HashMap<>();
Map<String, Integer> counts = new HashMap<>();
try (var rs = RecordingFile.open(file)) {
for (var event : rs) {
String name = event.getEventType().getName();
counts.merge(name, 1, Integer::sum);
if (event.hasValue("duration")) {
long ns = event.getDuration("duration").toNanos();
durations.merge(name, ns, Long::sum);
}
}
}
// 打印结果
System.out.println("=== Most Frequent ===");
counts.entrySet().stream()
.sorted((a, b) -> b.getValue() - a.getValue())
.limit(10).forEach(e ->
System.out.printf(" %s: %d%n", e.getKey(), e.getValue()));
System.out.println("=== Slowest ===");
durations.entrySet().stream()
.sorted((a, b) -> (int)(b.getValue() - a.getValue()))
.limit(10).forEach(e ->
System.out.printf(" %s: %d ms%n", e.getKey(), e.getValue() / 1_000_000));
}
}
实战场景
GC 停顿排查
bash
java -XX:StartFlightRecording:\
filename=gc.jfr,\
events="jdk.GC*,jdk.YoungGarbageCollection,jdk.OldGarbageCollection" -jar app.jar
JMC 里重点看:
- GC Pause 总时长和频率
- Young GC 频率是否过高
- 对象晋升(Promotion)是否频繁
接口慢请求定位
先写个简单的耗时日志 AOP:
java
@Aspect
@Component
public class TimingAspect {
@Around("@annotation(GetMapping)")
public Object trace(ProceedingJoinPoint p) throws Throwable {
var start = System.currentTimeMillis();
try {
return p.proceed();
} finally {
var cost = System.currentTimeMillis() - start;
if (cost > 1000) {
System.out.printf("[SLOW] %s: %d ms%n", p.getSignature(), cost);
}
}
}
}
慢请求出现后,配合 JFR 定位具体瓶颈:
| JFR 事件 | 对应问题 |
|---|---|
jdk.ExecutionSample |
CPU 热点 |
jdk.FileRead/Write |
磁盘 IO 慢 |
jdk.SocketRead/Write |
网络 IO 慢 |
jdk.JavaMonitorEnter |
锁等待 |
内存泄漏
开启对象分配事件:
jdk.ObjectAllocationInNewTLAB
jdk.ObjectAllocationOutsideTLAB
jdk.OldGarbageCollection
老年代回收频率异常升高 + 堆大小持续增长,基本就是泄漏了。配合 jfr print --events "jdk.OldGarbageCollection" 看回收模式,再导出堆 dump 定位泄漏对象。
生产环境注意事项
1. 资源限制
bash
-XX:FlightRecorderOptions=maxchunksize=100M,memorysize=50M
2. 持续录制 + 轮转
bash
java -XX:StartFlightRecording=\
filename=/opt/jfr/app.jfr,\
dumponexit=true,\
maxage=7d,\
maxsize=1G,\
settings=production -jar app.jar
3. 权限控制
yaml
export COM_SUN_JDK_JFR_OPTIONS="security-manager=true"
4. 告警联动
java
@Scheduled(fixedRate = 60000)
public void alertIfNeeded() {
// 读取 JFR 事件,异常时推送到告警平台
}
总结
JFR 不是什么新东西,但确实是「平时用不上,出事能救命」的工具。
Spring Boot 集成后,启用成本不高。生产环境按需开启,配合资源限制和轮转策略,遇到问题直接看录制文件,比猜日志高效得多。
几个建议:
- 先记几个常用 JFR 事件名,用到再查文档
- 持续录制 + 7 天轮转,出问题有数据可查
- 配合 JMC 可视化,分析效率更高