全系列目录:通过 JFR 与日志深入探索 JVM - 总览篇
我个人有个习惯,对于要用的一个新的框架,新的中间件等等,我一般不太信任它的官网“吹”的优点以及性能测试,我一般会一边使用一边自己测试,并且猜想其内部实现并结合源码搞清楚它的实现原理以及一些“坑”(这些“坑”并不是说这些框架或者中间件有什么毛病,而是因为官网浮夸的“吹”以及有些事情说一半留一半导致用户有误解),在这之后我才敢放心使用。
所以呢,我想先将 JFR 实现的基本原理提前说明白,这样可以让大家先有个整体印象,搞明白为何这么配置就对线上 JVM 影响小,从而放心使用。
JFR 记录开始:每个 JVM 进程可以同时启用多个 JFR 记录采集,可以在 JVM 启动的时候利用 JVM 启动参数启用 JFR 记录,也可以通过jcmd
动态开启 JFR 记录采集,也可以在程序内通过代码开启采集。
JFR 记录结束:可以启动时指定在采集多久后结束,也可以通过jcmd
动态关闭 JFR 记录采集,也可以在程序内通过代码结束采集。在结束时,可以指定让 JFR 记录 dump 到一个文件中。JFR 记录也会随着 JVM 的结束而结束。
JFR 记录分析:可以随时通过jcmd
动态将 JFR 记录 dump 到一个文件中,或者通过代码程序中执行 dump,进行后续分析。注意 dump 并不会结束一个 JFR 记录,并且 dump 最多只能 dump 出上次 dump 到现在的所有记录。
JFR 记录实时分析:可以通过 JFR Stream 实现对于 JFR 记录的实时消费与处理。
在 JFR中,一切皆为事件(Event):
这些 Event 在某些特定的时间点或者特定的场景下产生,每个事件都有事件类型,开始时间,结束时间,发生事件的线程,事件发生的线程堆栈还有 Event 数据体组成。Event 数据体不同的 Event 数据不同,例如 CPU 负载,Event 发生之前还有之后的 Java 堆大小, 获取锁的线程 ID 等等。需要注意的是,并不是所有事件都要采集结束时间,发生事件的线程或者事件发生的线程堆栈,例如那种定时采样事件,就不需要记录这些。
首先,按照采集规则,可以分为三类:
按照事件类型,又可以分为:
这里先用一个事件作为例子,简单介绍下存储,具体存储结构的详细介绍,请参考后面的章节。
Class Load Event
0000FC10 : 98 80 80 00 87 02 95 ae e4 b2 92 03 a2 f7 ae 9a 94 02 02 01 8d 11 00 00
0000FC10
: 文件位置98 80 80 00
: Event 大小87 02
: Event ID95 ae e4 b2 92 03
: 时间戳a2 f7 ae 9a 94 02
: 持续时间02
: 线程 ID01
: 堆栈 ID8d 11
: 加载的类00
: 定义类的 ClassLoader00
: 初始化类的 ClassLoader可以看出,JFR 的事件,是一个非常紧凑的存储方式,为了节省空间,对于同一类型的数字,根据大小变长存储。同时,没有存储字段名称。最后,对于一些公共的字段,例如上面这个事件定义类的 ClassLoader
这样的字段,保留的是指向元空间的指针。的这样保证了节省空间,对于 JFR 这种存储流程来说,节约空间对于提升性能是很重要的,下一小节就会说明为何这样。
这里仅仅是举个例子,实际使用中,我们肯定不会去这么看每个 Event 的,而是通过可视化工具 JMC 去看,这个我们后面会讲到。这些 Event 也可以被应用消费解析,解析的工具类也是 Java 内置的,后面也会讲到。
首先,Event 肯定是多线程产生的,这点显而易见。如果 Event 记录要保证全局有序,那么肯定需要多线程向一个指定队列或者缓存输出,那么不可避免的会涉及到锁争用,这样是很低效的。 Event本身带时间戳,那么可不可以在最后读取的时候进行排序?在一个线程内,生成的 Event 肯定是有序的;那么多线程产生的 Event, 就可以看成一个又一个的有序集合。最后,针对这些有序集合的每个元素进行整体排序,算法上快很多。所以我们没有必要在 Event 产生的时候就进行整体排序。
在 JFR 中,所有的 Event,会先存储到每个线程自己的线程 JFR 缓冲(Thread Buffer)中;在这个 Buffer 满了之后,会将 Buffer 的内容刷入全局 JFR 缓冲(Global Buffer )中;Global Buffer 是一个环形 Buffer,保存着所有线程发送来的 Thread Buffer 中的内容。当这个环形 Buffer 存储到达上限之后,根据配置,会选择丢弃或者刷入文件。
可以看出,不同的 Buffer 之间的数据不会有任何重叠。并且某一块数据,要么就是在内存中,要么就是在磁盘上,不会两个地方都存在,那么这样会带来数据丢失的问题:
这里涉及到的概念说明:
由于 Event 产生是每个线程独立产生的,生成事件就像打印日志一样,这些事件是尽可能地节省了空间的,可以让各种缓冲不那么快变满。同时,通过从 线程 JFR 缓冲 -> 全局 JFR 缓冲的流程大大减少了并发争用。并且,写入临时文件也是在全局 JFR 缓冲满了之后才刷入文件,减少了文件 IO。
通过上面的分析,我们可以知道,只要 Event 的产生没有瓶颈,那么 JFR 记录是很快的。但是,如果配置不当,Event 的产生很可能产生瓶颈,原因是:
对于可能进入安全点的 Event,或者是开启了堆栈采集的 Event,一定要控制好采集的频率或者事件的时间阈值限制,减少采集次数,如果过快,或者在业务高峰时采集的事件过多,肯定会影响性能。