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
2
# 加上以下的这两个参数即可开启对应的JFR功能
-XX:+UnlockCommercialFeatures -XX:+FlightRecorder

0x01_02 解锁JFR特征

1
2
#解锁JFR记录功能权限
jcmd $pid VM.unlock_commercial_features

注意:命令行中的pid需要替换为实际应用的进程ID,可使用psjps等命令获取。后面命令中出现pid需要替换为实际应用的进程ID,可使用ps、jps等命令获取。后面命令中出现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.jfcdefault.jfc,当然你可以定制自己的jmc,但需要将自己的jmc文件必须保存在上面所说目录下面。

0x03 命令解释

jcmd命令中包含了操作JFR的所有操作,现在对操作以及参数进行详细解释。

我们可以使用jcmd help命令去了解对应命令行的使用解释。例如,查看一个JFR.check的命令行使用方式,指定对应的进程ID(本文中使用5361),使用如下命令行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(base) ➜  jfr jcmd 72865 help JFR.check
72865:
JFR.check
Checks running JFR recording(s)

Impact: Low

Permission: java.lang.management.ManagementPermission(monitor)

Syntax : JFR.check [options]

Options: (options must be specified using the <key> or <key>=<value> syntax)
name : [optional] Recording name, e.g. \"My Recording\" or omit to see all recordings (STRING, no default value)
recording : [optional] Recording number, or omit to see all recordings (JLONG, -1)
verbose : [optional] Print event settings for the recording(s) (BOOLEAN, false)

通过英文的意思,我们可以了解到各个参数的使用方式,以及对应含义和注意事项。

下面我们进入正题,与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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package cn.openmind.jfr;

import java.util.ArrayList;
import java.util.List;

/**
* @author joyven
* @date 2022-04-2022/4/28 上午11:13
**/
public class JFRTest {
public static void main(String[] args) {
List<Object> items = new ArrayList<>(2);
try {
while (true){
items.add(new Object());
}
} catch (OutOfMemoryError e){
System.out.println(e.getMessage());
}
assert items.size() > 0;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
}

使用启动命令:

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
2
3
4
5
6
7
8
(base) ➜  2022 tree
.
└── cn
└── openmind
└── jfr
└── JFRTest.class

3 directories, 1 file

特别说明:将堆内存设置为128m,这样更容易抛出异常。

如果是在IDEA编辑器中启动,配置JVM为:-Xmx128m -Xms128m -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:StartFlightRecording=duration=200s,filename=flight.jfr即可。

IDEA中开启JFR

当项目启动完成,并发生 OutOfMemoryError 异常后,可以在目录下会发现一个名为flight.jfr的文件,将该文件拖至JDK Mission Control中,会自动进行分析。

JFR文件

下图展示JDK Mission Control分析结果:现在我们来具体分析这张图的结果:

Application Halts
从图中可以得知,应用程序内存的使用,在5s内迅速跑满,然后触发GC操作,是什么方法导致内存使用如此迅速呢?根据 Method Profiling 的分析结果,可以看出时ArrayList的拷贝操作导致,如下图:

由以上操作我们可以快速定位到内存溢出的方法块,其他块的使用大家可以自行进行分析的时候查看,都是可视化的界面非常方便。使用JFR的时候最后不要定义太大的时间块,或者需要切分小块的时间块进行分析,因为大的时间块,会导致JFR文件十分巨大,本地进行分析的时候,也会产生卡顿或者电脑内存不够,导致本地机子卡死,无法分析。


参考资料

  • Monitoring Java Applications with Flight Recorder
  • Java性能权威指南
  • JDK Mission Control