如何启动JFR(Java Flight Recorder)

官方的JDK21的JFR介绍: https://docs.oracle.com/javacomponents/jmc-5-4/jfr-runtime-guide/comline.htm# https://docs.oracle.com/en/java/javase/21/docs/specs/man

官方的JDK21的JFR介绍:

https://docs.oracle.com/javacomponents/jmc-5-4/jfr-runtime-guide/comline.htm#

https://docs.oracle.com/en/java/javase/21/docs/specs/man/jfr.html

1.JCMD 启动JFR

1.1 JCMD启动JFR任务

使用jcmd启动JFR的最简单的用法如下:

jcmd <pid> JFR.start name=jfr duration=120s filename=/tmp/jfr.jfr maxsize=250MB

它代表了启动一个name为jfr的JFR任务且JFR任务持续时间为120s,并且到120s过期之后会自动将JFR数据导出到/tmp/jfr.jfr,maxsize=250MB(默认就是250MB)代表JFR文件最多执行250MB时将会自动结束。

如果遇到了以下问题,需要将用户切换到启动目标进程的用户下(必须是进程目标用户才能attach并使用jcmd工具),再次执行dump即可。(例如su tomcat)

com.sun.tools.attach.AttachNotSupportedException: Unable to open socket file /proc/397/root/tmp/.java_pid397: target process 397 doesn't respond within 10500ms or HotSpot VM not loaded
        at jdk.attach/sun.tools.attach.VirtualMachineImpl.<init>(VirtualMachineImpl.java:99)
        at jdk.attach/sun.tools.attach.AttachProviderImpl.attachVirtualMachine(AttachProviderImpl.java:58)
        at jdk.attach/com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:207)
        at jdk.jcmd/sun.tools.jcmd.JCmd.executeCommandForPid(JCmd.java:113)
        at jdk.jcmd/sun.tools.jcmd.JCmd.main(JCmd.java:97)

1.2 对JFR任务进行dump

在duration时间过期之前,如果想要dump下来name=jfr这个任务的JFR文件,可以使用如下的命令进行随时进行提前dump。

jcmd <pid> JFR.dump name=jfr

如果我指定duration=120s,那么我在第10s执行dump的结果就是[0, 10s]之内的JFR采集的结果,如果我在第20s执行dump的结果就是[0, 20s]之内的JFR的采集的结果。

JFR任务dump的结果会存放到哪里呢?

  • 可以通过JFR.dump时,指定filename代表JFR数据dump的文件位置;
  • 如果JFR.dump不指定filename的话,会沿用JFR.start时的filename;
  • 如果JFR.start时也没指定filename的话,将会根据时间戳自动生成一个文件名进行dump。

1.3 提前结束JFR任务

如果在duration到期之前,可以使用JFR.stop命令提前关闭JFR,指定name=jfr代表关闭name=jfr这样的一个JFR任务。

jcmd <pid> JFR.stop name=jfr

1.4 JFR指定采样的事件

在JAVA_HOME/lib/jfr/目录下,存放了两个jfr的事件配置的配置文件,一个叫default.jmc,一个叫profile.jmc。

通过JFR.start默认使用的是default.jfc,如果需要更详细的事件采样,可以使用profile.jfc,指定配置文件可以基于settings参数进行配置。

jcmd <pid> JFR.start name=jfr duration=120s filename=/tmp/jfr.jfr maxsize=250MB settings=profile

如果需要自定义采样的名称的话,可以自定义settings配置文件

jcmd <pid> JFR.start name=jfr duration=120s filename=/tmp/jfr.jfr maxsize=250MB settings=/path/to/settings.jfc

1.5 基于SpringBootActuator暴露快速JFR端点

@WebEndpoint(id = "jfr")
public class JfrEndpoint {

    private static final Logger logger = LoggerFactory.getLogger(JfrEndpoint.class);

    /**
     * 加锁
     */
    private final Lock lock = new ReentrantLock();

    private final int timeoutSeconds;

    public JfrEndpoint() {
        this(10);
    }

    public JfrEndpoint(int timeoutSeconds) {
        this.timeoutSeconds = timeoutSeconds;
    }

    /**
     * 跑JFR任务
     *
     * @param fileName jfr任务输出的文件
     * @param duration 采样时间间隔(默认为120s), 当时间过期时JFR事件数据会自动dump到这个文件
     * @param settings settings(自定义jfr配置文件, 可以是default/profile/...)
     * @return JFR任务输出的文件
     */
    @ReadOperation
    public ResponseEntity<Object> profiler(@Nullable String fileName,
                                           @Nullable Long duration,
                                           @Nullable String settings) {
        final String fileNameToUse = Optional.ofNullable(fileName).orElse("profiler.jfr");
        final Long durationToUse = Optional.ofNullable(duration).orElse(120L);
        final String settingsToUse = Optional.ofNullable(settings).orElse("default");
        try {
            if (lock.tryLock(timeoutSeconds, TimeUnit.MILLISECONDS)) {
                try {
                    // 在Java10可以通过RuntimeMXBean直接获取pid, Java8只能通过切取
                    final String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
                    final ProcessBuilder processBuilder = new ProcessBuilder();

                    final String destinationFilePath = String.format("/tmp/%s", fileNameToUse);
                    Files.deleteIfExists(Paths.get(destinationFilePath));

                    processBuilder.command("jcmd", pid, "JFR.start", "name=jfr", String.format("duration=%ss", durationToUse), String.format("filename=%s", destinationFilePath), String.format("settings=%s", settingsToUse));

                    final Process process = processBuilder.start();
                    try (InputStream eis = process.getErrorStream(); InputStream is = process.getInputStream()) {
                        final String message = StreamUtils.copyToString(is, StandardCharsets.UTF_8);
                        final String errorMessage = StreamUtils.copyToString(eis, StandardCharsets.UTF_8);
                        logger.info("JfrEndpoint.profiler, {}", message);
                        if (!process.waitFor(10, TimeUnit.SECONDS)) {
                            throw new IllegalStateException(String.format("jcmd process start jfr error, %s", errorMessage));
                        }
                    }
                    TimeUnit.SECONDS.sleep(durationToUse);
                    return ResponseEntity.ok()
                            // 下载附件的文件名称(使用WebEndpointResponse实现不了这个功能)
                            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileNameToUse + "\"")
                            .contentType(MediaType.APPLICATION_OCTET_STREAM)
                            .body(new FileSystemResource(destinationFilePath));
                } finally {
                    lock.lock();
                }
            }
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("jfr lock error");
        } catch (Throwable ex) {
            logger.error("jfr profile error", ex);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}

2.通过JVM启动参数启动JFR

JVM启动方式配置方式和JCMD基本上完全一致,区别在于JCMD当中,指定多个参数是使用空格分割的自定义参数,命令行启动参数的方式启动是使用的,分隔多个自定义参数。

参考命令如下:

-XX:StartFlightRecording=name=jfr_profiler,maxage=1d,maxsize=1GB,filename=/home/q/www/default.qunar.com/logs/profiler.jfr,duration=600s,dumponexit=true

3.两种启动JFR方式对比

启动方式 动态性 能否采集启动时数据 对线上机器性能影响 排查问题便利性
CMD方式启动 JVM运行时动态开启 不能(JCMD一般在Java应用启动之后才能用) 小,指定机器开启 不一定能抓到问题现场,只能抓一些时机进行分析
JVM参数启动 JVM启动时指定 比较大,对一个环境当中的每台机器均生效,会占用机器一些CPU和内存 可以,JFR随时在线上跑着,对于一些特殊的线上问题很方便排查
Comment