JVM的JFR/JMC(飞行记录仪)低开销诊断:实现生产环境的性能Profiling
大家好!今天我们来聊聊Java虚拟机(JVM)自带的强大工具:Java Flight Recorder (JFR) 和 Java Mission Control (JMC)。它们提供了一种低开销的方式,在生产环境中对Java应用程序进行性能Profiling和诊断。 传统的Profiling工具往往会对应用程序的性能产生较大的影响,使得在生产环境中使用变得困难。JFR/JMC的出现,旨在解决这个问题,它以极低的性能损耗,记录JVM运行时的各种事件,帮助我们定位性能瓶颈、内存泄漏、死锁等问题。
1. JFR:JVM内部的“黑匣子”
Java Flight Recorder(JFR)是JVM内置的事件记录框架。它记录了JVM在运行时的各种事件,例如:
- CPU 使用情况: 线程占用 CPU 的时间,系统调用等。
- 内存分配: 对象创建、垃圾回收、内存泄漏等。
- I/O 操作: 文件读写、网络通信等。
- 锁竞争: 线程等待锁的时间、锁的持有者等。
- 方法执行: 方法调用、执行时间等。
- GC: 垃圾回收的频率、持续时间、回收类型等。
JFR的设计目标是低开销。它通过在JVM内部进行采样和事件缓冲,最大限度地减少对应用程序性能的影响。通常情况下,JFR的开销低于1%。
1.1 启动JFR
启动 JFR 的方式有多种,最常见的是通过命令行参数。以下是一些示例:
-
启动时启用JFR:
java -XX:StartFlightRecording=duration=60s,filename=myrecording.jfr,settings=profile my.application.MainClassStartFlightRecording: 启动 JFR 录制。duration: 录制时长,单位可以是s(秒)、m(分钟)、h(小时)、d(天)。filename: 录制文件的名称。settings: 指定 JFR 的配置。profile配置提供了更详细的信息,而default配置则开销更低。
-
运行时启动JFR:
可以使用
jcmd工具在 JVM 运行时启动 JFR。首先,需要找到 JVM 的进程 ID:jps然后,使用
jcmd启动 JFR:jcmd <pid> JFR.start duration=60s filename=myrecording.jfr settings=profile<pid>: JVM 的进程 ID。JFR.start: 启动 JFR 录制。
-
使用代码控制JFR:
在JDK 11及更高版本中,可以使用
jdk.jfrAPI直接控制JFR的启动、停止等。import jdk.jfr.*; import jdk.jfr.consumer.*; import java.nio.file.*; import java.time.*; import java.util.List; public class JFRExample { public static void main(String[] args) throws Exception { Path recordingFile = Paths.get("myrecording.jfr"); // 启动 JFR 录制 try (Recording recording = new Recording()) { recording.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1)); // 采样 CPU 负载 recording.enable("jdk.GarbageCollection"); // 启用 GC 事件 recording.setDuration(Duration.ofSeconds(10)); // 录制时长 recording.setDestination(recordingFile); // 录制文件 recording.start(); // 模拟一些工作 for (int i = 0; i < 1000000; i++) { Math.sqrt(i); } recording.dump(recordingFile); } // 读取 JFR 录制文件 try (var events = RecordingFile.readAllEvents(recordingFile)) { for (RecordedEvent event : events) { if (event.getEventType().getName().equals("jdk.CPULoad")) { System.out.println("CPU Load: " + event.getDouble("jvmUser") + "%"); } else if (event.getEventType().getName().equals("jdk.GarbageCollection")) { System.out.println("GC: " + event.getString("name")); } } } } }这个例子演示了如何通过代码启动 JFR 录制,并读取录制文件中的事件。
1.2 JFR 配置
JFR 提供了两种预定义的配置:
default: 默认配置,开销最低,记录的信息较少。profile: 提供更详细的信息,但开销相对较高。
除了预定义的配置外,还可以自定义 JFR 配置。自定义配置允许我们精确地控制哪些事件被记录,以及事件的采样频率。
JFR配置使用 .jfc 文件描述,可以自己编辑配置。 例如一个自定义配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration version="2.0" label="My Custom Config" description="A custom configuration for JFR">
<event name="jdk.CPULoad">
<setting name="enabled">true</setting>
<setting name="period">1 s</setting>
</event>
<event name="jdk.GarbageCollection">
<setting name="enabled">true</setting>
</event>
<event name="jdk.ObjectAllocationOutsideTLAB">
<setting name="enabled">true</setting>
<setting name="threshold">10 ms</setting>
</event>
<event name="jdk.SocketRead">
<setting name="enabled">true</setting>
<setting name="threshold">20 ms</setting>
</event>
</configuration>
使用自定义配置启动 JFR:
java -XX:StartFlightRecording=duration=60s,filename=myrecording.jfr,settings=/path/to/myconfig.jfc my.application.MainClass
1.3 JFR事件类型
JFR 记录的事件类型非常丰富,涵盖了 JVM 运行时的各个方面。一些常用的事件类型包括:
| 事件类型 | 描述 |
|---|---|
jdk.CPULoad |
CPU 使用情况,包括 JVM 用户态、系统态和空闲时间。 |
jdk.GarbageCollection |
垃圾回收事件,包括 GC 类型、持续时间、回收前后内存使用情况等。 |
jdk.ObjectAllocationOutsideTLAB |
在线程本地分配缓冲区(TLAB)之外分配的对象。 |
jdk.SocketRead |
Socket 读取事件,包括读取的字节数、持续时间等。 |
jdk.FileRead |
文件读取事件,包括读取的字节数、持续时间等。 |
jdk.ThreadPark |
线程阻塞事件,例如等待锁、等待 I/O 等。 |
jdk.JavaMonitorEnter |
线程进入 Java 监视器(synchronized 块)事件。 |
jdk.ExecuteableList |
记录编译后的可执行代码的信息,可以帮助定位热点代码。 |
2. JMC:强大的可视化分析工具
Java Mission Control(JMC)是一个用于分析 JFR 数据的可视化工具。它可以帮助我们:
- 查看 JFR 数据: 以图形化的方式展示 JFR 记录的事件。
- 分析性能瓶颈: 快速定位 CPU 占用高、内存泄漏、锁竞争等问题。
- 诊断内存泄漏: 分析堆转储文件,查找内存泄漏的根源。
- 监控 JVM 运行时状态: 实时监控 JVM 的 CPU 使用率、内存使用率、线程状态等。
2.1 安装和启动 JMC
JMC 通常包含在 JDK 中,位于 $JAVA_HOME/bin/jmc。如果没有安装,可以从 Oracle 官网下载。
启动 JMC 后,可以看到一个主界面,可以连接到本地或远程的 JVM,并打开 JFR 录制文件。
2.2 JMC 主要功能
- Overview(概述): 提供 JFR 数据的概览,包括 CPU 使用率、内存使用率、GC 活动等。
- Memory(内存): 展示内存使用情况,包括堆大小、堆使用率、GC 活动等。可以分析 GC 的频率、持续时间,以及不同代的内存使用情况。
- Threads(线程): 展示线程活动,包括线程状态、CPU 使用率、锁竞争等。可以分析线程阻塞的原因,以及锁竞争的激烈程度。
- I/O: 展示 I/O 活动,包括文件读写、网络通信等。可以分析 I/O 操作的延迟,以及 I/O 瓶颈。
- Method Profiling (方法分析): 展示方法的调用次数和执行时间,可以快速定位热点方法。
- Locks(锁): 展示锁竞争情况,包括锁的持有者、等待者、等待时间等。可以分析死锁的发生,以及锁竞争的根源。
- Events(事件): 以表格的形式展示 JFR 记录的所有事件。可以根据事件类型、时间范围等进行过滤和排序。
- Automated Analysis(自动分析): JMC 会自动分析 JFR 数据,并给出一些建议,例如内存泄漏、锁竞争等。
2.3 使用 JMC 分析 JFR 数据
下面通过一个简单的示例,演示如何使用 JMC 分析 JFR 数据。
-
启动 JFR 录制:
java -XX:StartFlightRecording=duration=60s,filename=myrecording.jfr,settings=profile my.application.MainClass -
运行应用程序:
运行
my.application.MainClass应用程序,使其产生一些 CPU 负载、内存分配等。 -
打开 JFR 录制文件:
在 JMC 中,选择 "File" -> "Open File…",打开
myrecording.jfr文件。 -
分析 JFR 数据:
- Overview: 查看 CPU 使用率、内存使用率、GC 活动等。如果 CPU 使用率较高,可以进一步分析线程活动,查找 CPU 占用高的线程。如果内存使用率较高,可以进一步分析 GC 活动,查找内存泄漏的根源。
- Memory: 查看堆大小、堆使用率、GC 活动等。如果 GC 频率较高,可以尝试调整 JVM 的堆大小,或者优化应用程序的内存使用。
- Threads: 查看线程状态、CPU 使用率、锁竞争等。如果线程阻塞较多,可以进一步分析锁竞争情况,查找锁竞争的根源。
- Method Profiling: 查看方法的调用次数和执行时间,快速定位热点方法。
2.4 JMC 插件
JMC 支持插件扩展,可以安装一些第三方插件,增强 JMC 的功能。例如,可以安装一些用于分析特定框架(如 Spring、Hibernate)的插件。
3. 生产环境下的JFR实践
在生产环境中使用 JFR 需要注意以下几点:
- 低开销: 选择合适的 JFR 配置,尽量减少对应用程序性能的影响。可以使用
default配置,或者自定义配置,只记录必要的事件。 - 持续监控: 可以配置 JFR 持续录制,例如每天生成一个 JFR 文件。这样可以在出现问题时,快速找到相关的 JFR 数据。
- 自动化分析: 可以编写脚本,定期分析 JFR 数据,并生成报告。这样可以及时发现潜在的问题,并采取相应的措施。
- 权限控制: 需要对 JFR 录制文件进行权限控制,防止敏感信息泄露。
3.1 案例分析:定位CPU占用过高问题
假设生产环境的某个Java应用CPU占用率持续偏高,影响了服务的响应速度。我们可以通过JFR来定位问题。
-
开启JFR录制:
jcmd <pid> JFR.start duration=300s filename=/tmp/cpu_high.jfr settings=profile这里录制5分钟的JFR数据,使用
profile配置,尽可能记录更详细的信息。 -
使用JMC分析:
打开
cpu_high.jfr文件,首先查看Overview,确认CPU使用率确实很高。然后进入"Threads"视图,按照CPU时间排序,可以找到占用CPU时间最多的线程。如果发现某个线程一直在执行某个方法,那么这个方法很可能就是CPU瓶颈。可以进一步查看"Method Profiling"视图,确认该方法的调用次数和执行时间。
如果发现大量的线程处于WAITING或BLOCKED状态,那么很可能存在锁竞争或者死锁。可以进一步查看"Locks"视图,分析锁的持有者和等待者。
-
代码分析与优化:
根据JMC的分析结果,定位到具体的代码,进行代码分析和优化。例如,可以优化算法,减少计算量;可以减少锁的竞争,避免死锁;可以使用缓存,减少IO操作。
3.2 案例分析:定位内存泄漏问题
假设生产环境的某个Java应用出现内存泄漏,导致JVM频繁进行Full GC,影响了服务的稳定性。
-
开启JFR录制:
jcmd <pid> JFR.start duration=3600s filename=/tmp/memory_leak.jfr settings=profile这里录制1小时的JFR数据,使用
profile配置。 -
使用JMC分析:
打开
memory_leak.jfr文件,首先查看"Memory"视图,确认Full GC的频率很高。然后查看"Allocation"标签页,可以查看对象的分配情况,包括分配的对象类型、大小、分配线程等。可以按照对象类型排序,找到分配数量最多的对象类型。如果某个对象类型的数量持续增长,且没有被垃圾回收,那么很可能存在内存泄漏。
还可以使用JMC的"Heap Dump"功能,生成堆转储文件,然后使用MAT等工具进行分析,找到内存泄漏的根源。
-
代码分析与优化:
根据JMC和MAT的分析结果,定位到具体的代码,进行代码分析和优化。例如,可以检查对象的生命周期,确保不再使用的对象能够被及时回收;可以避免静态集合持有对象引用,导致对象无法被回收;可以使用WeakReference或SoftReference,避免内存泄漏。
4. JFR/JMC 与其他Profiling工具的对比
| 工具 | 开销 | 数据粒度 | 可视化 | 易用性 | 适用场景 |
|---|---|---|---|---|---|
| JFR/JMC | 低 | 细粒度 | 强大 | 较高 | 生产环境,需要低开销的Profiling,以及详细的JVM运行时数据。 |
| JProfiler/YourKit | 中等 | 细粒度 | 强大 | 较高 | 开发和测试环境,需要详细的Profiling数据,以及强大的可视化功能。 |
| VisualVM | 中等 | 细粒度 | 一般 | 中等 | 开发和测试环境,需要基本的Profiling功能,以及简单的可视化功能。 |
| Async Profiler | 低 | 细粒度 | 一般(需要配合Flame Graph) | 较高 | 生产环境,需要低开销的CPU Profiling,以及Flame Graph可视化。 |
| Btrace | 可配置 | 细粒度 | 无 | 较高 | 需要在线诊断和监控,可以动态地插入和移除探针,但需要谨慎使用,避免对性能产生过大的影响。 |
5. 高效利用 JFR/JMC 的关键点
- 明确目标: 在启动 JFR 之前,明确要解决的问题,例如 CPU 占用高、内存泄漏、锁竞争等。
- 选择合适的配置: 根据目标选择合适的 JFR 配置,尽量减少对应用程序性能的影响。
- 分析关键指标: 重点关注 CPU 使用率、内存使用率、GC 活动、线程状态等关键指标。
- 结合代码分析: 将 JFR 分析结果与代码结合起来,找到问题的根源。
- 持续学习: 持续学习 JFR/JMC 的新功能和最佳实践,提高分析效率。
6. 总结一下JFR/JMC的价值
JFR/JMC作为JVM自带的Profiling工具,以其低开销、易用性、强大的可视化分析能力,成为生产环境性能诊断的重要利器。掌握JFR/JMC的使用,能够帮助我们快速定位和解决各种性能问题,提高Java应用的稳定性和性能。