因为jfr.start离线录文件无法捕捉转瞬即逝的问题,而recordingstream流式订阅可实时消费内存中事件,但需显式调用start()或startasync()、严格匹配事件全名、设置setreuse(false)防数据覆盖,并注意jvm版本兼容性。

为什么不能只用 JFR.start 录文件再离线分析
生产环境的问题往往转瞬即逝:一次 3 秒的 GC 尖刺、一个被 jdk.virtualThreadPinned 卡住的虚拟线程、或某次异常高的方法采样热点,等你导出 .jfr 文件再加载进 JMC,现场早已过去。离线录制本质是“事后验尸”,而 Event Streaming 是“实时听诊”。它绕过磁盘 I/O 和文件序列化开销,直接从 JVM 内存环形缓冲区拉取事件流,实测在高吞吐服务中 CPU 增益稳定在 0.8% 以内(对比默认 disk=false 的录制模式)。
RecordingStream 启动时必须显式调用 start() 或 startAsync()
这是最容易漏掉的一步——声明 RecordingStream 实例本身不会触发任何事件消费。JDK 16 的 EventStream 接口设计为“懒启动”,必须手动激活:
- 用
stream.start()在当前线程阻塞运行,适合嵌入简单监控 Agent 场景 - 用
stream.startAsync()启动后台守护线程,推荐用于 Spring Boot 应用,避免阻塞主线程 - 不调用任一方法 → 事件照常产生,但你的 Consumer 永远收不到
示例片段:
RecordingStream stream = new RecordingStream();
stream.onEvent("jdk.CPULoad", event -> {
double usage = event.getDouble("jvmUser");
if (usage > 0.9) alert("High CPU: " + usage);
});
stream.startAsync(); // 必须加这一行
过滤事件名要严格匹配,大小写和前缀都不能错
JFR 事件全名是带包路径的,比如不是 "CPULoad",而是 "jdk.CPULoad";"MethodSample" 正确写法是 "jdk.MethodSample";虚拟线程相关事件从 JDK 19 才有,JDK 16 中根本不存在 "jdk.virtualThreadPinned" —— 若误配,Consumer 永远静默。
- 查可用事件列表:运行
jcmd <pid> VM.native_memory summary</pid>后执行jcmd <pid> JFR.check</pid>,输出里会列出当前启用的事件名 - 常见有效事件(JDK 16):
"jdk.GarbageCollection"、"jdk.ThreadPark"、"jdk.JavaMonitorEnter"、"jdk.ExecutionSample" - 别依赖文档硬记,用
onEvent(Consumer)先通收,打印event.getEventType().getName()看真实名称
流式消费时要注意 setReuse(false) 和 onFlush()
默认情况下,JFR 复用同一个 RecordedEvent 对象填充不同事件数据,如果你在 Consumer 里把 event 缓存到队列或发往 Kafka,后续事件会覆盖前值,导致数据错乱。
- 必须调用
stream.setReuse(false),确保每个事件都是独立对象 - 高频场景下(如每秒上千次
jdk.ExecutionSample),建议用onFlush(Runnable)批量处理:等 JVM 把一批事件打包好再统一推送,降低网络/IO 压力 - 注意
onFlush不代表事件已“确定”——JFR 只保证同一批内有序,跨批次不保证全局顺序
真正上线时,最易被忽略的是 JVM 版本兼容性:JDK 16 的 RecordingStream API 在 JDK 17+ 中虽保留,但部分事件字段名有变更(如 "stackTrace" → "javaStackTrace"),若监控系统需长期兼容多版本,务必封装一层字段提取适配逻辑。











