鲲鹏社区首页
中文
注册
Java Flight Recorder - 事件机制详解

Java Flight Recorder - 事件机制详解

DevKit

发表于 2022/01/06

26

1. 本篇文章中的源码大部分来自 openjdk8u262

2. 本文出发点是梳理 JFR 的事件机制, 侧重点在于理解而非应用

3. 使用上可以不要太过拘泥细节,可以具体问题具体分析。

Prologue

1. 对于 JFR 我们有着怎样的预期

它是一个辅助分析工具,我们希望借助它,尽可能低开销地收集运行时数据,从而辅助对 JVM 可能存在的故障、性能瓶颈进行分析。

我们结合 JFR 的 Goals 来看:

(1)提供基于生成和消费数据作为事件的 API

(2)提供缓存机制和二进制数据格式

(3)允许配置和过滤事件

(4)为 OS、JVM、JDK 库提供相应的事件

从中,我们能粗略地获取这些信息 :

(1)事件以自描述的二进制形式 (.jfr) 被保存着

(2)事件中包含了数据,事件 ≈ 数据

(3).jfr 文件 => read by some Provided API => 重现运行时数据 [=> 可视化 ]

我们想尝试了解 JFR 的事件驱动机制,具体点就是回答几个问题:

一个事件何时产生 / 启动监控? 经历了怎样的路径? 如何被保存? 保存到哪里?

2. JFR 是事件驱动的 (JEP 328)

本节主要是一些前置信息 (假如你有所了解,可以快速浏览或者跳过本节内容):JVM 行为基本都是 Event,如类加载对应着 Class Load Event, 垃圾回收对应 GC Event;Event 主要由 timestamp, event name, additional info, data 这几部分组成。Event 收集四类事件的信息:

(1)Instant Event,发生就收集(e.g. Thread Start ...)

(2)Duration Event,持续收集一段时间(e.g. GC Event ...)

(3)Timed Event,收集超过指定时间的事件

(4)Sample Event,按频率采样

以 JFR 的 Class Load Event 为例, 看看一个事件的结构。(共计 24 bytes)

<memory address> : 98 80 80 00 87 02 95 ae e4 b2 92 03 a2 f7 ae 9a 94 02 02 01 8d 11 00 00

- Event Size : 98 80 80 00
- Event ID : 87 02
- TimeStamp : 95 ae e4 b2 92 03
- Duration : a2 f7 ae 9a 94 02
- Thread ID : 02
- Stack trace ID : 01
- PayLoad(记录的数据,fields 取决于各个 Event 类型):
  - 加载的类 : 8d 11
  - 定义类的 ClassLoader : 00
  - 初始化类的 ClassLoader : 00

多个线程都会产生 Event,线程通过无锁(Lock-free)设计记录事件。线程将事件首先写入到 ThreadLocalBuffer(简称 TLB), TLB 被填满后,将被转存到 Global buffer(circular),对于较旧的数据,可以通过配置,选择丢弃或者写入磁盘,以便连续保存历史记录。示意图如下所示:


注意:TLB、Global Buffer 和磁盘文件中的事件记录不会相互备份,未及时转存的数据可能发生丢失,本文不会就这点展开阐述。

前置内容已经交代清楚,接着回到正轨。

一个事件的生命周期


以下是枯燥乏味的一堆代码,但是不得不看。首先来看 JFR 的结构,如下图所示:


肉眼可见的一堆钩子,这些 hook 用于记录对应的触发事件。

我们简单地挑一个 Thread Start 的事件,关注一下它的整个被触发到被记录的过程。在线程创建并执行时会调用记录 JFR 事件,代码如下:

可见当一个新的 Java 线程被创建时,只要开启了 JFR, 那么就会执行上述代码;

接着看一下 on_thread_start 干了什么:


在此,我们看到了一个事件 EventThreadStart,并且在事件中设置信息后被提交。

在 JEP 328 中有一个更为简单直接例子,如下:


无需太过关心其内容。我们只需关注这个事件生成的结构:


这里的 EventType 定义于 jfrEventClass.hpp, 该文件是编译时生成的,简单贴一下生成逻辑,可以参考 Makefile 文件,如下 (同样无需在意太多细节):


回到主旋律,继续来看事件的结构和成员函数,如下:


其中最为重要的成员函数是 JfrEvent::commit 方法,用于提交事件,代码如下:


在函数中,最后一段代码, 也是核心所在,用于真正记录事件:


这下,就可以很容易地和 第 1 节 的内容对应上了,特别是其中的事件模型的图片:


Epilogue

用户是否可以自定义一个 JFR 事件?注意点有哪些?

这里通过 JEP 328 里的例子(稍微有点改动),来展示如何自定义 JFR 事件。


通过编译后直接执行如下命令:

$> java -XX:StartFlightRecording,filename=event.jfr Test

可以得到如下日志信息:

Started recording 1. No limit specified, using maxsize=250MB as default.
Use jcmd 57980 JFR.dump name=1 to copy recording data to file.

日志可以通过标准的 API 进行解析,下面通过一个简单代码解析上面生成的事件,代码如下:


编译运行

$> java Viewer | less

可以得到如下结果。


相信此时你已经对 JFR 的事件机制有了个不错的感觉。

实际上 JFR 的使用一般配合 JMC[1] 使用,在 JMC 中通过页面可以得到统计信息,更有助于判断系统的运行情况。

参考

[1] https://adoptopenjdk.net/jmc.html

编者按:Java Flight Recorder(简称为 JFR)曾经是 Oracle JDK 商业版的附属组件,在 JDK 11 中被正式开始开源,后又被移植到 JDK8 中。JFR 本身对运行期系统的侵入性很小,同时又能提供相对准确和丰富的运行期信息;合理使用该工具可以极大地提高工作效率。本文介绍 JFR 剖析的事件机制,希望能帮助大家从原理上理解 JFR 并正确使用 JFR。

本页内容