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

当分配出来 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 做一些操作。

image

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

9. OpenJDK HotSpot TLAB 相关源代码分析

如果这里看的比较吃力,可以直接看第 10 章,热门 Q&A,里面有很多大家常问的问题

9.1. TLAB 类构成

线程初始化的时候,如果 JVM 启用了 TLAB(默认是启用的, 可以通过 -XX:-UseTLAB 关闭),则会初始化 TLAB。

TLAB 包括如下几个 field (HeapWord* 可以理解为堆中的内存地址):
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

//静态全局变量 static size_t _max_size; // 所有 TLAB 的最大大小 static int _reserve_for_allocation_prefetch; // CPU 缓存优化 Allocation Prefetch 的保留空间,这里先不用关心 static unsigned _target_refills; //每个 GC 周期内期望的重填次数 //以下是 TLAB 的主要构成 field HeapWord* _start; // TLAB 起始地址,表示堆内存地址都用 HeapWord* HeapWord* _top; // 上次分配的内存地址 HeapWord* _end; // TLAB 结束地址 size_t _desired_size; // TLAB 大小 包括保留空间,表示内存大小都需要通过 size_t 类型,也就是实际字节数除以 HeapWordSize 的值 size_t _refill_waste_limit; // TLAB最大浪费空间,剩余空间不足分配浪费空间限制。在TLAB剩余空间不足的时候,根据这个值决定分配策略,如果浪费空间大于这个值则直接在 Eden 区分配,如果小于这个值则将当前 TLAB 放回 Eden 区管理并从 Eden 申请新的 TLAB 进行分配。 AdaptiveWeightedAverage _allocation_fraction; // 当前 TLAB 分配比例 EMA //以下是我们这里不用太关心的 field HeapWord* _allocation_end; // TLAB 真正可以用来分配内存的结束地址,这个是 _end 结束地址排除保留空间(预留给 dummy object 的对象头空间) HeapWord* _pf_top; // Allocation Prefetch CPU 缓存优化机制相关需要的参数,这里先不用考虑 size_t _allocated_before_last_gc; // 这个用于计算 图10 中的线程本轮 GC 分配空间的大小,记录上次 GC 时,线程分配的空间大小 unsigned _number_of_refills; // 线程分配内存数据采集相关,TLAB 剩余空间不足分配次数 unsigned _fast_refill_waste; // 线程分配内存数据采集相关,TLAB 快速分配浪费,快速分配就是直接在 TLAB 分配,这个在现在 JVM 中已经用不到了 unsigned _slow_refill_waste; // 线程分配内存数据采集相关,TLAB 慢速分配浪费,慢速分配就是重填一个 TLAB 分配 unsigned _gc_waste; // 线程分配内存数据采集相关,gc浪费 unsigned _slow_allocations; // 线程分配内存数据采集相关,TLAB 慢速分配计数 size_t _allocated_size; // 分配的内存大小 size_t _bytes_since_last_sample_point; // JVM TI 采集指标相关 field,这里不用关心 9.2. TLAB 初始化

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

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