JFR(Java Flight Recorder)使用教程
0x00 概念
JFR,全称为Java Flight Recorder,简称JFR,是Oracle JDK的商业特性,JFR数据是JVM的历史事件,用来诊断JVM的历史性能和操作,该数据是作为时间上的数据点(称为事件)记录的。是一种监控工具,可以在Java应用程序执行期间收集有关JVM事件的信息。JFR会开启一组事件,当有对应的事件发生时,就会保留相应的数据到文件中或者内存中(假如有开启缓存池),JMC可以显示这些事件——实时从JVM获取或者从文件中获取,JMC可以展示详细的JFR记录的数据。JFR对于被监控的应用程序来说,默认设置的性能开销很低:程序性能的1%以下,但是随着开始的事件或者记录线程增多,性能开销也会随之增多。
JFR有两种主要的概念:事件和数据流。
0x00_01 事件
JFR在Java应用运行时收集对应发生的事件,主要有三种类型的事件提供给JFR收集:
- 即时事件:一旦事件发生会立即进行数据记录
- 持续事件:如果持续时间超过指定阈值则进行数据记录
- 简单事件:用于记录应用所在系统的活跃指标(例如CPU,内存等)
0x00_02 数据流
JFR收集的事件包含大量数据,将这些数据保存在filename.jfr中,众所周知,磁盘I/O操作非常昂贵。因此,在将数据块刷新到磁盘之前,JFR使用各种缓存来存储收集的数据。因为加入缓存的原因,在某些情况下,JFR的数据存在丢失的可能性。如果发生丢失数据的情况,JFR会尝试通知输出文件,丢失了一部分的信息。
0x01 使用
0x01_01 开启配置
在应用启动配置的JVM参数中,增加以下两个配置参数开启JFR:
1 | # 加上以下的这两个参数即可开启对应的JFR功能 |
0x01_02 解锁JFR特征
1 | #解锁JFR记录功能权限 |
注意:命令行中的pid同理。
0x01_03 记录数据点
使用jcmd命令行开启一个记录线程,duration记录的时间段,默认为0s,代表无限制,以下代码使用2分钟,表示记录2分钟结束。filename表示保存的文件名。
1 | jcmd $pid JFR.start name=myrec settings=profile delay=20s duration=2m filename=flight.jfr |
上面命令中,name、settings、delay等参数均可使用默认。
此外,需要注意的是如果settings不设置,默认使用default.jfc。jfc文件的目录在$JAVA_HOME的jre/lib/jfr
,默认有两个jfc:default.jfc
和default.jfc
,当然你可以定制自己的jmc,但需要将自己的jmc文件必须保存在上面所说目录下面。
0x03 命令解释
jcmd命令中包含了操作JFR的所有操作,现在对操作以及参数进行详细解释。
我们可以使用jcmd help
命令去了解对应命令行的使用解释。例如,查看一个JFR.check的命令行使用方式,指定对应的进程ID(本文中使用5361),使用如下命令行:
1 | (base) ➜ jfr jcmd 72865 help JFR.check |
通过英文的意思,我们可以了解到各个参数的使用方式,以及对应含义和注意事项。
下面我们进入正题,与JFR关联的jcmd命令有以下四种:
JFR.start – 启动一个新的JFR记录线程。
JFR.check – 检查正在运行的JFR记录线程。
JFR.stop – 停止一个指定的JFR记录线程。
JFR.dump – 拷贝一个指定的JFR记录线程的内容进入文件中。
每个命令行都有对应的参数,现在一一介绍对应的参数详解
- JFR.start
参数 | 说明 | 值类型 | 默认值 |
---|---|---|---|
name | 记录线程的名字 | String | 无 |
settings | 服务端模版 | String | 无 |
defaultrecording | 开始默认记录 | Boolean | False |
delay | 开始记录的延迟时间 | Time | 0s |
duration | 记录的时长 | Time | 0s(表示永远,不中断) |
filename | 记录的名称 | String | |
compress | 使用GZip压缩记录的结果文件 | Boolean | False |
maxage | 缓冲区数据的最长使用期限 | Time | 0s代表没有时间期限制 |
maxsize | 缓存容量的最大数量 | Long | 0代表没有最大大小 |
- JFR.check
参数 | 说明 | 值类型 | 默认值 |
---|---|---|---|
name | 记录线程的名字 | String | 无 |
recording | 记录线程的ID值 | Long | 1 |
verbose | 是否打印详细数据信息 | Boolean | False |
- JFR.stop
参数 | 说明 | 值类型 | 默认值 |
---|---|---|---|
name | 记录线程的名字 | String | 无 |
recording | 记录线程的ID值 | Long | 1 |
discard | 抛弃记录数据 | Boolean | 无 |
copy_to_file | 拷贝记录数据到文件 | String | 无 |
compress_copy | GZip压缩“copy_to_file”的文件 | Boolean | False |
- JFR.dump
参数 | 说明 | 值类型 | 默认值 |
---|---|---|---|
name | 记录线程的名字 | String | 无 |
recording | 记录线程的ID值 | Long | 1 |
copy_to_file | 拷贝记录数据到文件 | String | 无 |
compress_copy | GZip压缩“copy_to_file”的文件 | Boolean | False |
0x04 实战
介绍完命令行的使用方式以及参数详解,现在我们就来进行实战使用JFR。这里有一个注意点就是,虽然JFR被设计为在JVM和应用程序的性能的影响会小,但是最好设置持续时间(duration),缓冲区数据的最长使用期限(maxage),限制收集的最大数据量(maxsize)。
首先定义个一个内存泄露的主程序,具体代码如下:
1 | package cn.openmind.jfr; |
使用启动命令:
1 | java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:StartFlightRecording=duration=200s,filename=flight.jfr -Xmx128m cn.openmind.jfr.JFRTest |
在命令行,我们运行的JFRTest是编译好的字节码,我们可以用tree命令来看看文件所在位置,执行代码是在包名所在位置,不是具体的class文件位置,事实上,jvm加载的包名其实就是路径,会把.
转为/
。我这里是IDEA编译生成的,如果不用IDEA,那么你可以选择javac编译源代码。
1 | (base) ➜ 2022 tree |
特别说明:将堆内存设置为128m,这样更容易抛出异常。
如果是在IDEA编辑器中启动,配置JVM为:-Xmx128m -Xms128m -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:StartFlightRecording=duration=200s,filename=flight.jfr
即可。
当项目启动完成,并发生 OutOfMemoryError 异常后,可以在目录下会发现一个名为flight.jfr的文件,将该文件拖至JDK Mission Control中,会自动进行分析。
下图展示JDK Mission Control分析结果:现在我们来具体分析这张图的结果:
Application Halts
从图中可以得知,应用程序内存的使用,在5s内迅速跑满,然后触发GC操作,是什么方法导致内存使用如此迅速呢?根据 Method Profiling 的分析结果,可以看出时ArrayList的拷贝操作导致,如下图:
由以上操作我们可以快速定位到内存溢出的方法块,其他块的使用大家可以自行进行分析的时候查看,都是可视化的界面非常方便。使用JFR的时候最后不要定义太大的时间块,或者需要切分小块的时间块进行分析,因为大的时间块,会导致JFR文件十分巨大,本地进行分析的时候,也会产生卡顿或者电脑内存不够,导致本地机子卡死,无法分析。
参考资料
- Monitoring Java Applications with Flight Recorder
- Java性能权威指南
- JDK Mission Control