概念
JFR(Java Flight Recorder,下文简称JFR)数据是JVM的历史事件,用来诊断JVM的历史性能和操作。是一种监控工具,可以在Java应用程序执行期间收集有关JVM事件的信息。JFR会开启一组事件,当有对应的事件发生时,就会保留相应的数据到文件中或者内存中(假如有开启缓存池),JMC可以显示这些事件——实时从JVM获取或者从文件中获取,JMC可以展示详细的JFR记录的数据。JFR对于被监控的应用程序来说,默认设置的性能开销很低:程序性能的1%以下,但是随着开始的事件或者记录线程增多,性能开销也会随之增多。
JFR有两种主要的概念:事件和数据流
事件
JFR在Java应用运行时收集对应发生的事件,主要有三种类型的事件提供给JFR收集:
- 即时事件:一旦事件发生会立即进行数据记录
- 持续事件:如果持续时间超过指定阈值则进行数据记录
- 简单事件:用于记录应用所在系统的活跃指标(例如CPU,内存等)
数据流
JFR收集的事件包含大量数据,将这些数据保存在filename.jfr中,众所周知,磁盘I/O操作非常昂贵。因此,在将数据块刷新到磁盘之前,JFR使用各种缓存来存储收集的数据。因为加入缓存的原因,在某些情况下,JFR的数据存在丢失的可能性。如果发生丢失数据的情况,JFR会尝试通知输出文件,丢失了一部分的信息。
前期准备
需要下载安装对应的Java Mission Control,自行下载即可,我本地安装了Java Mission Control 8(文章发布时的最新版),JMC可以同JFR结合使用,JMC可以将JFR生成的XXX.jfr文件可视化展示出来。数据展示非常丰富。在文章实战后面会详细解释。
使用
使用配置参数开启:
# 加上以下的这两个参数即可开启对应的JFR功能
java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder
复制代码
在Tomcat中配置对应的参数不生效,也不知道是什么原因,如果有解决的小伙伴可以在评论区
使用命令行开启
使用jcmd命令行解锁JFR功能权限。
jcmd process_id VM.unlock_commercial_features 解锁JFR记录功能权限
复制代码
使用jcmd命令行开启一个记录线程,duration记录的时间段,默认为0s,代表无限制,以下代码使用200s,表示记录200s结束。filename表示保存的文件名。
jcmd激活 jcmd process_id JFR.start duration=100s filename=flight.jfr (JDK 11版本前需要先激活对应的功能)
复制代码
命令行详解
jcmd命令中包含了操作JFR的所有操作,现在对操作以及参数进行详细解释。
首先,假如你不了解一些命令行的使用规则,你可以使用jcmd help命令去了解对应命令行的使用解释。例如,查看一个JFR.check的命令行使用方式,指定对应的进程ID(本文中使用5361),使用如下命令行:
jcmd 5361 help JFR.check
# 命令行返回值
5361:
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 |
实战
介绍完命令行的使用方式以及参数详解,现在我们就来进行实战使用JFR。这里有一个注意点就是,虽然JFR被设计为在JVM和应用程序的性能的影响会小,但是最好设置持续时间(duration),缓冲区数据的最长使用期限(maxage),限制收集的最大数据量(maxsize)。
首先定义个一个内存泄露的主程序,具体代码如下:
public static void main(String[] args) {
List<Object> items = new ArrayList<>(1);
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());
}
}
复制代码
使用启动命令:
java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder
-XX:StartFlightRecording=duration=200s,filename=flight.jfr
复制代码
当项目启动完成,并发生 OutOfMemoryError 异常后,可以在目录下会发现一个名为flight.jfr的文件,将该文件拖至JDK Mission Control中,会自动进行分析。下图展示JDK Mission Control分析结果:
现在我们来具体分析这张图的结果:
- 图片右侧红色的分数表示,该次分析的重点关注的部分,展开后都会进行详细的描述造成原因,例如第一点
Application Halts
表示由于GC造成应用程序停顿时间过长,比例偏高。如果实在不想看,复制贴贴放进翻译软件中,也会明白大致的意思。 - 图片的左侧的侧边栏表示每个分析块的详细记录,有内存,有GC,有I/O等等,有红色小感叹号的是重点需要查看的部分。例如我们查看内存这一块
从图中可以得知,应用程序内存的使用,在5s内迅速跑满,然后触发GC操作,是什么方法导致内存使用如此迅速呢?根据Method Profiling
的分析结果,可以看出时ArrayList的拷贝操作导致,如下图:
由以上操作我们可以快速定位到内存溢出的方法块,其他块的使用大家可以自行进行分析的时候查看,都是可视化的界面非常方便。使用JFR的时候最后不要定义太大的时间块,或者需要切分小块的时间块进行分析,因为大的时间块,会导致JFR文件十分巨大,本地进行分析的时候,也会产生卡顿或者电脑内存不够,导致本地机子卡死,无法分析。
参考资料
Monitoring Java Applications with Flight Recorder
Java性能权威指南
JDK Mission Control