hexon
发布于 2025-12-20 / 15 阅读
0

7、扩展:什么是JFR?Spring中是如何利用它的?

在前面看Spring源码的过程中,有下面的代码:

public AnnotationConfigApplicationContext() {
    StartupStep createAnnotatedBeanDefReader = getApplicationStartup().start("spring.context.annotated-bean-reader.create");
    // BeanDefinition读取器,可以注册BeanDefinition,也可以读取BeanDefinition
    // 本质上就是BeanDefinitionRegistry,多了ConditionEvaluator,以及注册了一些默认的BeanPostProcessor
    this.reader = new AnnotatedBeanDefinitionReader(this);
    createAnnotatedBeanDefReader.end();

    // BeanDefinition扫描器,负责扫描
    this.scanner = new ClassPathBeanDefinitionScanner(this);
}

后面的代码中也会看到getApplicationStartup()的start和end方法调用,本文我们来细节了解下这个是干什么的。

本节的代码是直接从上节拷贝的SpringBoot工程

源码初探

顺着getApplicationStartup()方法点进去,可以知道Spring默认提供了两个实现类,其中DefaultApplicationStartup是比较简单的,它的start方法就是返回对象本身,end方法是空方法,所以默认相当于getApplicationStartup().start()与getApplicationStartup().end()这两份行代码都没有做任何的事情。

而另一个实现类,就是我们今天要说的JFR。

什么是JFR?

JFR全称为JDK Flight Recorder,是JVM中的一个事件记录器,相当于飞机中的数据飞行记录器(黑匣子),用于记录应用程序在工作过程中发生的各种事件的情况,默认内置了很多事件,比如GC事件:

上图表示做了4次Young GC,1次Old GC,共5次,每次GC的开始时间、总耗时都有记录。

GC Identifier表示本次GC的ID,可以通过它查看GC前后的变化:

默认还有很多其他事件(比如:操作系统层面内存的、CPU的),本文就不一一分析的,大家可自行体验和研究。

开启这个会有一定的性能损耗

如何开启JFR机制?

默认没有开启JFR,想要使用JFR,只需要添加-XX:StartFlightRecording这个JVM参数即可,添加了参数后,在启动应用程序时就会出现以下提示:

表示可以在程序运行过程中使用jcmd 9600 JFR.dump name=1 filename=FILEPATH命令来生成jfr文件,从而可以查看在执行命令之前应用程序内部发生的各种事件的情况。

其中,9600表示当前应用程序的进程id,FILEPATH需要改成jfr文件存放的路径和文件名,应用程序运行过程中,可以多次执行以上命令,生成的jfr文件如果重名会直接覆盖。比如用如下命令将jfr文件导出存放在项目根目录的test.jfr文件中:

jcmd 9600 JFR.dump name=1 filename=test.jfr

可以利用JMC工具或在IDEA中直接双击jfr文件就能看到具体的事件信息了。

如何自定义JFR事件?

定义如下:

/**
 * 自定义JFR事件
 **/
@Category("类别A")
@Label("标签A")
public class MyJfrEvent extends Event {

    @Label("事件名称")
    private String eventName; // 自定义属性,可以通过构造器或者setter赋值

    public MyJfrEvent(String eventName) {
        this.eventName = eventName;
    }

}

使用如下:

@Service
public class UserService {

    public String test() {

        MyJfrEvent event = new MyJfrEvent("事件A");

        event.begin();

        // 业务逻辑

        event.commit();

        return "success";
    }
}

多次调用test方法,重新导出jfr文件,最终效果如下:

我们可以自定义事件的主类别、子类别、事件的属性等,默认情况下会自动记录事件的时间和线程情况。

Spring中的JFR事件

在Spring源码工程的测试模块中,我们也添加-XX:StartFlightRecording这个JVM参数再运行测试,代码如下:

public class Main {
	public static void main(String[] args) throws IOException {
		AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);
		UserService userService = applicationContext.getBean(UserService.class);
		userService.test();
		
		System.in.read(); // 保持阻塞,防止进程停止
	}
}

此时导出jfr文件,打开查看:

看不到Spring的事件分类,这是因为默认用的是ApplicationStartup.DEFAULT相当于没有记录事件。只要我们改造下代码的写法即可:

public class Main {
	public static void main(String[] args) throws IOException {
		AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
		applicationContext.setApplicationStartup(new FlightRecorderApplicationStartup());
		applicationContext.register(MyConfig.class);
		applicationContext.refresh();

		UserService userService = applicationContext.getBean(UserService.class);
		userService.test();

		System.in.read(); // 保持阻塞,防止进程停止
	}
}

可以看到Spring中也利用了JFR事件机制定义了很多事件,比如

其中spring.context.refresh表示刷新容器的总耗时,spring.context.instantiate表示创建某个Bean的耗时,tags中记录了对应Bean的名字,这样,我们也可以通过JFR机制查看Spring在启动过程中各个步骤的耗时情况。