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

首先,TLAB 的初始大小,应该和每个 GC 内需要对象分配的线程个数相关。但是,要分配的线程个数并不一定是稳定的,可能这个时间段线程数多,下个阶段线程数就不那么多了,所以,需要用 EMA 的算法采集每个 GC 内需要对象分配的线程个数来计算这个个数期望

接着,我们最理想的情况下,是每个 GC 内,所有用来分配对象的内存都处于对应线程的 TLAB 中。每个 GC 内用来分配对象的内存从 JVM 设计上来讲,其实就是 Eden 区大小。在 最理想的情况下,最好只有Eden 区满了的时候才会 GC,不会有其他原因导致的 GC,这样是最高效的情况。Eden 区被用光,如果全都是 TLAB 内分配,也就是 Eden 区被所有线程的 TLAB 占满了,这样分配是最快的。

然后,每轮 GC 分配内存的线程个数以及大小是不一定的,如果一下子分配一大块会造成浪费,如果太小则会频繁从 Eden 申请 TLAB,降低效率。这个大小比较难以控制,但是我们可以限制每个线程究竟在一轮 GC 内,最多从 Eden 申请多少次 TLAB,这样对于用户来说更好控制。

最后,每个线程分配的内存大小,在每轮 GC 并不一定稳定,只用初始大小来指导之后的 TLAB 大小,显然不够。我们换个思路,每个线程分配的内存和历史有一定关系因此我们可以从历史分配中推测,所以每个线程也需要采用 EMA 的算法采集这个线程每次 GC 分配的内存,用于指导下次期望的 TLAB 的大小。

综上所述,我们可以得出这样一个近似的 TLAB 计算公式

每个线程 TLAB 初始大小 = Eden区大小 / (线程单个 GC 轮次内最多从 Eden 申请多少次 TLAB * 当前 GC 分配线程个数 EMA)

GC 后,重新计算 TLAB 大小 = Eden区大小 / (线程单个 GC 轮次内最多从 Eden 申请多少次 TLAB * 当前 GC 分配线程个数 EMA)

接下来,我们来详细分析 TLAB 的整个生命周期的每个流程。

8.1. TLAB 初始化

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

image

初始化时候会计算 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 初始期望大小计算

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wppzys.html