全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜)(中)

简介: 全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜)(中)

8.1. TLAB 初始化


线程初始化的时候,如果 JVM 启用了 TLAB(默认是启用的, 可以通过 -XX:-UseTLAB 关闭),则会初始化 TLAB,在发生对象分配时,会根据期望大小申请 TLAB 内存。同时,在 GC 扫描对象发生之后,线程第一次尝试分配对象的时候,也会重新申请 TLAB 内存。我们先只关心初始化,初始化的流程图如 图08 所示:


微信图片_20220625115513.jpg


初始化时候会计算 TLAB 初始期望大小。这涉及到了 TLAB 大小的限制

  • TLAB 的最小大小:通过MinTLABSize指定
  • TLAB 的最大大小:不同的 GC 中不同,G1 GC 中为大对象(humongous object)大小,也就是 G1 region 大小的一半。因为开头提到过,在 G1 GC 中,大对象不能在 TLAB 分配,而是老年代。ZGC 中为页大小的 8 分之一,类似的在大部分情况下 Shenandoah GC 也是每个 Region 大小的 8 分之一。他们都是期望至少有 8 分之 7 的区域是不用退回的减少选择 Cset 的时候的扫描复杂度。对于其他的 GC,则是 int 数组的最大大小,这个和之前提到的填充 dummy object 有关,后面会提到详细流程。

之后的流程里面,无论何时,TLAB 的大小都会在这个 TLAB 的最小大小 到 TLAB 的最大大小 的范围内,为了避免啰嗦,我们不会再强调这个限制~~~!!!之后的流程里面,无论何时,TLAB 的大小都会在这个 TLAB 的最小大小 到 TLAB 的最大大小 的范围内,为了避免啰嗦,我们不会再强调这个限制~~~!!!之后的流程里面,无论何时,TLAB 的大小都会在这个 TLAB 的最小大小 到 TLAB 的最大大小 的范围内,为了避免啰嗦,我们不会再强调这个限制~~~!!! 重要的事情说三遍~

TLAB 期望大小(desired size) 在初始化的时候会计算 TLAB 期望大小,之后再 GC 等操作回收掉 TLAB 需要重计算这个期望大小。根据这个期望大小,TLAB 在申请空间的时候每次申请都会以这个期望大小作为基准的空间作为 TLAB 分配空间。


8.1.1. TLAB 初始期望大小计算

图08 所示,如果指定了 TLABSize,就用这个大小作为初始期望大小。如果没有指定,则按照如下的公式进行计算:

堆给TLAB的空间总大小/(当前有效分配线程个数期望*重填次数配置)

  1. 堆给 TLAB 的空间总大小:堆上能有多少空间分配给 TLAB,不同的 GC 算法不一样,但是大多数 GC 算法的实现都是 Eden 区大小,例如:
  1. 传统的已经弃用的 Parallel Scanvage 中,就是 Eden 区大小。参考:parallelScavengeHeap.cpp
  2. 默认的G1 GC 中是 (YoungList 区域个数减去 Survivor 区域个数) * 区域大小,其实就是 Eden 区大小。参考:g1CollectedHeap.cpp
  3. ZGC 中是 Page 剩余空间大小,Page 类似于 Eden 区,是大部分对象分配的区域。参考:zHeap.cpp
  4. Shenandoah GC 中是 FreeSet 的大小,也是类似于 Eden 的概念。参考:shenandoahHeap.cpp


  1. 当前有效分配线程个数期望:这是一个全局 EMA,EMA 是什么之前已经说明了,是一种计算期望的方式。有效分配线程个数 EMA 的最小权重是 TLABAllocationWeight。有效分配线程个数 EMA 在有线程进行第一次有效对象分配的时候进行采集,在 TLAB 初始化的时候读取这个值计算 TLAB 期望大小。
  2. TLAB 重填次数配置(refills time):根据 TLABWasteTargetPercent 计算的次数,公式为。TLABWasteTargetPercent 的意义其实是限制最大浪费空间限制,为何重填次数与之相关后面会详细分析。


8.1.2. TLAB 初始分配比例计算

图08 所示,接下来会计算TLAB 初始分配比例。

线程私有分配比例 EMA:与有效分配线程个数 EMA对应,有效分配线程个数 EMA是对于全局来说,每个线程应该占用多大的 TLAB 的描述,而分配比例 EMA 相当于对于当前线程应该占用的总 TLAB 空间的大小的一种动态控制。

初始化的时候,分配比例其实就是等于 1/当前有效分配线程个数图08 的公式,代入之前的计算 TLAB 期望大小的公式,消参简化之后就是1/当前有效分配线程个数。这个值作为初始值,采集如线程私有的分配比例 EMA


8.1.3. 清零线程私有统计数据

这些采集数据会用于之后的当前线程的分配比例的计算与采集,从而影响之后的当前线程 TLAB 期望大小。


8.2. TLAB 分配


TLAB 分配流程如 图09 所示。


微信图片_20220625115531.jpg


8.2.1. 从线程当前 TLAB 分配

如果启用了 TLAB(默认是启用的, 可以通过 -XX:-UseTLAB 关闭),则首先从线程当前 TLAB 分配内存,如果分配成功则返回,否则根据当前 TLAB 剩余空间与当前最大浪费空间限制大小进行不同的分配策略。在下一个流程,就会提到这个限制究竟是什么。


8.2.2. 重新申请 TLAB 分配

如果当前 TLAB 剩余空间大于当前最大浪费空间限制(根据 图08 的流程,我们知道这个初始值为 期望大小/TLABRefillWasteFraction),直接在堆上分配。否则,重新申请一个 TLAB 分配。 为什么需要最大浪费空间呢?

当重新分配一个 TLAB 的时候,原有的 TLAB 可能还有空间剩余。原有的 TLAB 被退回堆之前,需要填充好 dummy object。由于 TLAB 仅线程内知道哪些被分配了,在 GC 扫描发生时返回 Eden 区,如果不填充的话,外部并不知道哪一部分被使用哪一部分没有,需要做额外的检查,如果填充已经确认会被回收的对象,也就是 dummy object, GC 会直接标记之后跳过这块内存,增加扫描效率。反正这块内存已经属于 TLAB,其他线程在下次扫描结束前是无法使用的。这个 dummy object 就是 int 数组。为了一定能有填充 dummy object 的空间,一般 TLAB 大小都会预留一个 dummy object 的 header 的空间,也是一个 int[] 的 header,所以 TLAB 的大小不能超过int 数组的最大大小,否则无法用 dummy object 填满未使用的空间。

但是,填充 dummy 也造成了空间的浪费,这种浪费不能太多,所以通过最大浪费空间限制来限制这种浪费。

新的 TLAB 大小,取如下两个值中较小的那个:

  • 当前堆剩余给 TLAB 可分配的空间,大部分 GC 的实现其实就是对应的 Eden 区剩余大小:
  • 传统的已经弃用的 Parallel Scanvage 中,就是 Eden 区剩余大小。参考:parallelScavengeHeap.cpp
  • 默认的G1 GC 中是当前 Region 中剩余大小,其实就是将 Eden 分区了。参考:g1CollectedHeap.cpp
  • ZGC 中是 Page 剩余空间大小,Page 类似于 Eden 区,是大部分对象分配的区域。参考:zHeap.cpp
  • Shenandoah GC 中是 FreeSet 的剩余大小,也是类似于 Eden 的概念。参考:shenandoahHeap.cpp


  • TLAB 期望大小 + 当前需要分配的空间大小

当分配出来 TLAB 之后,根据 ZeroTLAB 配置,决定是否将每个字节赋 0。在创建对象的时候,本来也要对每个字段赋初始值,大部分字段初始值都是 0,并且,在 TLAB 返还到堆时,剩余空间填充的也是 int[] 数组,里面都是 0。所以其实可以提前填充好。并且,TLAB 刚分配出来的时候,赋 0 也能利用好 Allocation prefetch 的机制适应 CPU 缓存行(Allocation prefetch 的机制会在另一个系列说明),所以可以通过打开 ZeroTLAB 来在分配 TLAB 空间之后立刻赋 0。


8.2.3. 直接从堆上分配

直接从堆上分配是最慢的分配方式。一种情况就是,如果当前 TLAB 剩余空间大于当前最大浪费空间限制,直接在堆上分配。并且,还会增加当前最大浪费空间限制,每次有这样的分配就会增加 TLABWasteIncrement 的大小,这样在一定次数的直接堆上分配之后,当前最大浪费空间限制一直增大会导致当前 TLAB 剩余空间小于当前最大浪费空间限制,从而申请新的 TLAB 进行分配。


8.3. GC 时 TLAB 回收与重计算期望大小


相关流程如 图10 所示,在 GC 前与 GC 后,都会对 TLAB 做一些操作。


微信图片_20220625115606.jpg


8.3.1. GC 前的操作

在 GC 前,如果启用了 TLAB(默认是启用的, 可以通过 -XX:-UseTLAB 关闭),则需要将所有线程的 TLAB 填充 dummy Object 退还给堆,并计算并采样一些东西用于以后的 TLAB 大小计算。


首先为了保证本次计算具有参考意义,需要先判断是否堆上 TLAB 空间被用了一半以上,假设不足,那么认为本轮 GC 的数据没有参考意义。如果被用了一半以上,那么计算新的分配比例,新的分配比例 = 线程本轮 GC 分配空间的大小 / 堆上所有线程 TLAB 使用的空间,这么计算主要因为分配比例描述的是当前线程占用堆上所有给 TLAB 的空间的比例,每个线程不一样,通过这个比例动态控制不同业务线程的 TLAB 大小。

线程本轮 GC 分配空间的大小包含 TLAB 中分配的和 TLAB 外分配的,从 图8、图9、图10 流程图中对于线程记录中的线程分配空间大小的记录就能看出,读取出线程分配空间大小减去上一轮 GC 结束时线程分配空间大小就是线程本轮 GC 分配空间的大小

最后,将当前 TLAB 填充好 dummy object 之后,返还给堆。


8.3.2. GC 后的操作

如果启用了 TLAB(默认是启用的, 可以通过 -XX:-UseTLAB 关闭),以及 TLAB 大小可变(默认是启用的, 可以通过 -XX:-ResizeTLAB 关闭),那么在 GC 后会重新计算每个线程 TLAB 的期望大小,新的期望大小 = 堆给TLAB的空间总大小 * 当前分配比例 EMA / 重填次数配置。然后会重置最大浪费空间限制,为当前 期望大小 / TLABRefillWasteFraction

相关文章
|
2月前
|
监控 算法 Java
jvm-48-java 变更导致压测应用性能下降,如何分析定位原因?
【11月更文挑战第17天】当JVM相关变更导致压测应用性能下降时,可通过检查变更内容(如JVM参数、Java版本、代码变更)、收集性能监控数据(使用JVM监控工具、应用性能监控工具、系统资源监控)、分析垃圾回收情况(GC日志分析、内存泄漏检查)、分析线程和锁(线程状态分析、锁竞争分析)及分析代码执行路径(使用代码性能分析工具、代码审查)等步骤来定位和解决问题。
|
7月前
|
缓存 Java
《JVM由浅入深学习九】 2024-01-15》JVM由简入深学习提升分(生产项目内存飙升分析)
《JVM由浅入深学习九】 2024-01-15》JVM由简入深学习提升分(生产项目内存飙升分析)
59 0
|
3月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
63 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
3月前
|
存储 Java PHP
【JVM】垃圾回收机制(GC)之引用计数和可达性分析
【JVM】垃圾回收机制(GC)之引用计数和可达性分析
90 0
|
6月前
|
Java
jmap 查看jvm内存大小并进行dump文件内存分析
jmap 查看jvm内存大小并进行dump文件内存分析
144 3
|
6月前
|
运维 监控 Java
(十)JVM成神路之线上故障排查、性能监控工具分析及各线上问题排错实战
经过前述九章的JVM知识学习后,咱们对于JVM的整体知识体系已经有了全面的认知。但前面的章节中,更多的是停留在理论上进行阐述,而本章节中则更多的会分析JVM的实战操作。
147 1
|
6月前
|
Arthas 监控 Java
JVM内存问题之使用gperftools分析JNI Memory泄漏的具体步骤是什么
JVM内存问题之使用gperftools分析JNI Memory泄漏的具体步骤是什么
147 2
|
5月前
|
监控 JavaScript Java
JVM源码级别分析G1发生FullGC元凶的是什么
线上系统遭遇频繁Old GC问题,监控显示出现多次“to-space exhausted”日志,这表明垃圾回收过程中因年轻代 Survivor 区或老年代空间不足导致对象晋升失败。通过 JVM 源码分析,此问题源于对象转移至老年代失败时,JVM 无法找到足够的空间存放存活对象。进一步排查发现大对象分配占用了预留空间,加剧了空间不足的情况。使用 JFR 分析工具定位到定期报表序列化导致大量大对象生成,通过改用堆外内存进行序列化输出,最终解决了频繁 Old GC 问题。
159 0
|
6月前
|
监控 Java 开发者
Java面试题:如何使用JVM工具(如jconsole, jstack, jmap)来分析内存使用情况?
Java面试题:如何使用JVM工具(如jconsole, jstack, jmap)来分析内存使用情况?
238 2
|
6月前
|
人工智能 Java
JVM内存问题之当老年代缓慢增加且Full GC无法清除时,应如何使用MAT进行分析
JVM内存问题之当老年代缓慢增加且Full GC无法清除时,应如何使用MAT进行分析
298 0