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

src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

//在TLAB慢分配被调用,当前 TLAB 放回堆 void ThreadLocalAllocBuffer::retire_before_allocation() { //将当前 TLAB 剩余空间大小加入慢分配浪费空间大小 _slow_refill_waste += (unsigned int)remaining(); //执行 TLAB 退还给堆,这个在后面 GC 的时候还会被调用用于将所有的线程的 TLAB 退回堆 retire(); } //对于 TLAB 慢分配,stats 为空 //对于 GC 的时候调用,stats 用于记录每个线程的数据 void ThreadLocalAllocBuffer::retire(ThreadLocalAllocStats* stats) { if (stats != NULL) { accumulate_and_reset_statistics(stats); } //如果当前 TLAB 有效 if (end() != NULL) { invariants(); //将用了的空间记录如线程分配对象大小记录 thread()->incr_allocated_bytes(used_bytes()); //填充dummy object insert_filler(); //清空当前 TLAB 指针 initialize(NULL, NULL, NULL); } } 9.4. GC 相关 TLAB 操作 9.4.1. GC 前

不同的 GC 可能实现不一样,但是 TLAB 操作的时机是基本一样的,这里以 G1 GC 为例,在真正 GC 前:

src/hotspot/share/gc/g1/g1CollectedHeap.cpp

void G1CollectedHeap::gc_prologue(bool full) { //省略其他代码 // Fill TLAB's and such { Ticks start = Ticks::now(); //确保堆内存是可以解析的 ensure_parsability(true); Tickspan dt = Ticks::now() - start; phase_times()->record_prepare_tlab_time_ms(dt.seconds() * MILLIUNITS); } //省略其他代码 }

为何要确保堆内存是可以解析的呢?这样有利于更快速的扫描堆上对象。确保内存可以解析里面做了什么呢?其实主要就是退还每个线程的 TLAB 以及填充 dummy object。

src/hotspot/share/gc/g1/g1CollectedHeap.cpp

void CollectedHeap::ensure_parsability(bool retire_tlabs) { //真正的 GC 肯定发生在安全点上,这个在后面安全点章节会详细说明 assert(SafepointSynchronize::is_at_safepoint() || !is_init_completed(), "Should only be called at a safepoint or at start-up"); ThreadLocalAllocStats stats; for (JavaThreadIteratorWithHandle jtiwh; JavaThread *thread = jtiwh.next();) { BarrierSet::barrier_set()->make_parsable(thread); //如果全局启用了 TLAB if (UseTLAB) { //如果指定要回收,则回收 TLAB if (retire_tlabs) { //回收 TLAB,调用 9.3.2.3. 当前 TLAB 放回堆 提到的 retire 方法 thread->tlab().retire(&stats); } else { //当前如果不回收,则将 TLAB 填充 Dummy Object 利于解析 thread->tlab().make_parsable(); } } } stats.publish(); } 9.4.2. GC 后

不同的 GC 可能实现不一样,但是 TLAB 操作的时机是基本一样的,这里以 G1 GC 为例,在 GC 后:

src/hotspot/share/gc/g1/g1CollectedHeap.cpp
_desired_size是什么时候变得呢?怎么变得呢?

void G1CollectedHeap::gc_epilogue(bool full) { //省略其他代码 resize_all_tlabs(); }

src/hotspot/share/gc/shared/collectedHeap.cpp

void CollectedHeap::resize_all_tlabs() { //需要在安全点,GC 会处于安全点的 assert(SafepointSynchronize::is_at_safepoint() || !is_init_completed(), "Should only resize tlabs at safepoint"); //如果 UseTLAB 和 ResizeTLAB 都是打开的(默认就是打开的) if (UseTLAB && ResizeTLAB) { for (JavaThreadIteratorWithHandle jtiwh; JavaThread *thread = jtiwh.next(); ) { //重新计算每个线程 TLAB 期望大小 thread->tlab().resize(); } } }

重新计算每个线程 TLAB 期望大小:
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp

void ThreadLocalAllocBuffer::resize() { assert(ResizeTLAB, "Should not call this otherwise"); //根据 _allocation_fraction 这个 EMA 采集得出平均数乘以Eden区大小,得出 TLAB 当前预测占用内存比例 size_t alloc = (size_t)(_allocation_fraction.average() * (Universe::heap()->tlab_capacity(thread()) / HeapWordSize)); //除以目标 refill 次数就是新的 TLAB 大小,和初始化时候的计算方法差不多 size_t new_size = alloc / _target_refills; //保证在 min_size 还有 max_size 之间 new_size = clamp(new_size, min_size(), max_size()); size_t aligned_new_size = align_object_size(new_size); log_trace(gc, tlab)("TLAB new size: thread: " INTPTR_FORMAT " [id: %2d]" " refills %d alloc: %8.6f desired_size: " SIZE_FORMAT " -> " SIZE_FORMAT, p2i(thread()), thread()->osthread()->thread_id(), _target_refills, _allocation_fraction.average(), desired_size(), aligned_new_size); //设置新的 TLAB 大小 set_desired_size(aligned_new_size); //重置 TLAB 最大浪费空间 set_refill_waste_limit(initial_refill_waste_limit()); } 10. TLAB 流程常见问题 Q&A

这里我会持续更新的,解决大家的各种疑问

10.1. 为何 TLAB 在退还给堆的时候需要填充 dummy object

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

10.2. 为何 TLAB 需要最大浪费空间限制

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

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