JFR应用之通过TLAB事件分析应用性能

2022年05月16日 900次浏览

吴斌华(白泽)

从Java对象空间分配策略说起

JVM在为对象分配内存空间的时候,考虑性能问题,并不会直接在堆中开辟内存空间,而是会尝试 栈上分配TLAB分配

栈上分配

  • 通过逃逸分析判断变量的引用范围,尝试将未逃逸的变量分配到栈空间(聚合量会先进行 标量替换
  • 分配在栈空间的对象,会在离开方法栈后,随之销毁,不需要等待GC,降低GC压力

TLAB分配

  • 多线程同时申请内存空间时,会出现内存争用的现象,此时JVM会使用CAS加锁,大大降低内存分配效率,影响多线程性能,并行的线程越多,性能影响越大
  • TLAB (Thread Local Allocation Buffer,线程本地分配缓冲区)即为每个线程在eden区开辟独立专用的内存区域,线程申请内存时会优先在自己对应的TLAB区申请,避免内存争用,提高多线程性能
  • 当TLAB区域将被填满(TLAB内分配失败,剩余空间小于申请空间),且剩余空间小于 最大浪费空间,会触发refill开辟新的TLAB区域,退回原有TLAB区域,refill过程是同步的
  • 当TLAB区域将被填满(TLAB内分配失败,剩余空间小于申请空间),且剩余空间大于 最大浪费空间,会触发TLAB外分配,TLAB外分配策略根据垃圾收集器的不同也有差异

分配流程

JVM默认开启栈上分配和TLAB分配,分配流程如下:

TLAB外分配策略根据垃圾收集器的不同也有差异,图中的流程并不覆盖所有情况

TLAB和应用性能的关系

  • 上文提到,TLAB可以避免内存开辟阻塞的问题,可以大大地提高多线程应用的性能
  • 换言之,TLAB外的内存分配会降低性能
  • 应用TLAB外的内存分配占比越大,应用的并行性能也就越低,TLAB外分配过多往往意味着应用中存在大对象
  • 频繁地TLAB refill同样会影响性能,正常情况下,在长时间运行后,基于EMA算法,TLAB refill和GC会达到一定的平衡,重分配次数下降。重分配过频,说明线程创建的对象存在不规则伸缩的情况

感知应用的TLAB行为

感知应用TLAB分配行为的手段比较有限,值得庆幸的是JFR默认提供了TLAB事件的记录

JFR(Java Flight Recorder)简述

什么是JFR

  • JFR(Java Flight Recordings),从名字上就可以看出,这是一套类似于飞机黑匣子的解决方案
  • 定义上是一个用于收集关于正在运行的Java应用程序的诊断和分析数据的工具。它集成到Java虚拟机(JVM)中,几乎不会导致任何性能开销,因此即使在负载沉重的生产环境中也可以使用它。当使用默认设置时,内部测试和客户反馈都表明性能影响小于1%。对于某些应用程序,它可能要低得多。--摘自官方文档
  • 本质上是一套记录和收集指定时间段内应用,JVM以及OS行为事件的解决方案,配合JFR分析工具(比如JMC)可以用于问题排查以及性能分析
  • JFR本来是一项商业特性,需要商业许可,前几年已经开放授权。从JDK11开始已经内置了JFR,JDK14更是增加了JFR事件流进一步增加了易用性。并且慢慢在低版本的OpenJDK也增加了这一特性
  • 海拍客生成环境使用的JDK是Tencent Kona 8,Kona 8在OpenJDK 8 的基础上增加了JFR特性

与内存dump的区别

  • 内存dump记录的是单一时刻的内存快照,而JFR记录的是某一时间区间
  • 内存dump关注内存相关的问题,而JFR全面地分析包括OS,内存,CPU,线程在内的几乎所有信息
  • 内存dump对应用影响较大,基本需要关闭流量路口再进行操作,JFR记录资源占用在1%-2%左右,可以直接在提供服务的节点上开启记录
  • 内存dump记录了整个内存的(存活)对象信息,文件往往较大,而短时间段内的JFR文件往往不超过100M
  • 由于内存dump记录的是当前的对象信息,当内存分析一些占比较小的内存问题时,排查会比较困难,有时候需要对比两个dump文件来发现那些持续增长的对象,而JFR可以记录指定时间内内存的分配量,是一个增量的信息统计,可以大大地降低内存问题排查的难度

如何收集JFR

收集JFR一般有三种方式:

  1. 使用JCMD命令
  2. 利用JMC工具远程收集
  3. 使用arthas dump收集

JFR中的TLAB事件

jdk.ObjectAllocationOutsideTLAB事件:即TLAB外分配事件,在线程申请内存空间发生TLAB外分配时触发

属性说明
startTime事件开始时间
objectClass触发本次事件的对象的类
allocationSize分配对象大小
eventThread事件发生所在线程
stackTrace事件发生所在堆栈

jdk.ObjectAllocationInNewTLAB事件:即TLAB重分配事件,在线程申请内存空间TLAB refill时触发

属性说明
startTime事件开始时间
objectClass触发本次事件的对象的类
allocationSize分配对象大小
tlabSize当前线程的 TLAB 大小
eventThread事件发生所在线程
stackTrace事件发生所在堆栈

通过JFR TLAB事件分析应用性能

以detail中台为例

  1. detail中台是一个并行性能要求比较高的应用,也就是说TLAB对detail的影响也比较大,下面以detail应用为例分析内存分配性能

  2. 采集TLAB以及线程事件,时间区间为两分钟,导出detail的jfr文件

  3. 自动分析结果中提到了一些优化点,简单说明下:

    1. 空闲物理内存过少:提示可用内存太少可能出现内存swap,影响性能
    2. DebugNonSafepoints:JVM没有开启DebugNonSafepoints选项,分析数据可能会不太精确
    3. 命令行选项检查:JVM参数中配置了一些非正式的和废弃的选项,会影响JVM的稳定性
    4. 堆栈深度设置:JFR采集过程中设置了最大堆栈深度,主要是为了避免JFR对应用的影响,这里提示了这个堆栈深度的设置会影响深度最终分析
    5. 结果中出现的一些占位符考虑是jmc bug或者是配置的影响,目前没有深究
  4. 总的来说yt-detail的性能表现还可以的,没有提示明显严重的问题

  5. 重点关注JVM内部-TLAB分配的部分

    1. 可以看到在JFR记录期间有9个线程出现了TLAB外内存分配
    2. 出现TLAB外分配的线程较少,说明整体内存分配的性能表现尚可,但三个业务线程池线程TLAB外分配占比较高需要重点关注下
    3. 切换到方法tab,发现TLAB外分配集中在char[] java.util.Arrays.copyOfRange(char[], int, int)方法上,说明有集中的性能问题
  6. 切换到事件浏览器页面

    1. 搜索TLAB事件,TLAB重分配681次,TLAB外分配21次
    2. 重点关注三个业务相关的线程的外分配也就是ThreadPool-*
    3. 该线程池主要负责模块构造
    4. 选中线程查看堆栈可以发现,几次TLAB外部内存分配都是由.BeanMap.of(Object)触发,这一块可能性能不太好
    5. 翻一下代码,的确是一个值得优化的点

拓展思考

  • 为什么应用在执行一段时间后性能会提高?
    • java应用在执行一段时间后变快的原因是方方面面的,契合本文的主题,TLAB也影响因素之一
    • 上文提到TLAB大小由EMA算法来决定,EMA需要进行一定次数的采样后才能趋于稳定,所以,应用刚启动时的TLAB大小往往变化频繁,运行一段时间后趋于稳定
  • 面向栈上分配和TLAB,如何提高我们的代码质量
    • 尽量避免大对象的使用,大对象无法利用栈上分配和TLAB的JVM优化,会带来显著的性能下降,当然对GC也十分不友好
    • 尽量避免变量逃逸,让变量处于合适的作用域中,这将有利于栈上分配以及锁消除