3.1. TLAB 快分配
src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp
inline HeapWord* ThreadLocalAllocBuffer::allocate(size_t size) { //验证各个内存指针有效,也就是 _top 在 _start 和 _end 范围内 invariants(); HeapWord* obj = top(); //如果空间足够,则分配内存 if (pointer_delta(end(), obj) >= size) { set_top(obj + size); invariants(); return obj; } return NULL; }
3.2. TLAB 慢分配
src/hotspot/share/gc/shared/memAllocator.cpp
HeapWord* MemAllocator::allocate_inside_tlab_slow(Allocation& allocation) const { HeapWord* mem = NULL; ThreadLocalAllocBuffer& tlab = _thread->tlab(); // 如果 TLAB 剩余空间大于 最大浪费空间,则记录并让最大浪费空间递增 if (tlab.free() > tlab.refill_waste_limit()) { tlab.record_slow_allocation(_word_size); return NULL; } //重新计算 TLAB 大小 size_t new_tlab_size = tlab.compute_size(_word_size); //TLAB 放回 Eden 区 tlab.retire_before_allocation(); if (new_tlab_size == 0) { return NULL; } // 计算最小大小 size_t min_tlab_size = ThreadLocalAllocBuffer::compute_min_size(_word_size); //分配新的 TLAB 空间,并在里面分配对象 mem = Universe::heap()->allocate_new_tlab(min_tlab_size, new_tlab_size, &allocation._allocated_tlab_size); if (mem == NULL) { assert(allocation._allocated_tlab_size == 0, "Allocation failed, but actual size was updated. min: " SIZE_FORMAT ", desired: " SIZE_FORMAT ", actual: " SIZE_FORMAT, min_tlab_size, new_tlab_size, allocation._allocated_tlab_size); return NULL; } assert(allocation._allocated_tlab_size != 0, "Allocation succeeded but actual size not updated. mem at: " PTR_FORMAT " min: " SIZE_FORMAT ", desired: " SIZE_FORMAT, p2i(mem), min_tlab_size, new_tlab_size); //如果启用了 ZeroTLAB 这个 JVM 参数,则将对象所有字段置零值 if (ZeroTLAB) { // ..and clear it. Copy::zero_to_words(mem, allocation._allocated_tlab_size); } else { // ...and zap just allocated object. } //设置新的 TLAB 空间为当前线程的 TLAB tlab.fill(mem, mem + _word_size, allocation._allocated_tlab_size); //返回分配的对象内存地址 return mem; }
3.2.1 TLAB最大浪费空间
TLAB最大浪费空间 _refill_waste_limit
初始值为 TLAB 大小除以 TLABRefillWasteFraction:src/hotspot/share/gc/shared/threadLocalAllocBuffer.hpp
size_t initial_refill_waste_limit() { return desired_size() / TLABRefillWasteFraction; }
每次慢分配,调用record_slow_allocation(size_t obj_size)
记录慢分配的同时,增加 TLAB 最大浪费空间的大小:
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
void ThreadLocalAllocBuffer::record_slow_allocation(size_t obj_size) { //每次慢分配,_refill_waste_limit 增加 refill_waste_limit_increment,也就是 TLABWasteIncrement set_refill_waste_limit(refill_waste_limit() + refill_waste_limit_increment()); _slow_allocations++; log_develop_trace(gc, tlab)("TLAB: %s thread: " INTPTR_FORMAT " [id: %2d]" " obj: " SIZE_FORMAT " free: " SIZE_FORMAT " waste: " SIZE_FORMAT, "slow", p2i(thread()), thread()->osthread()->thread_id(), obj_size, free(), refill_waste_limit()); } //refill_waste_limit_increment 就是 JVM 参数 TLABWasteIncrement static size_t refill_waste_limit_increment() { return TLABWasteIncrement; }
3.2.2. 重新计算 TLAB 大小
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp_desired_size
是什么时候变得呢?怎么变得呢?
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()); }
那是什么时候调用 resize
的呢?一般是每次** GC 完成的时候**。大部分的 GC 都是在gc_epilogue
方法里面调用,将每个线程的 TLAB 均 resize
掉。
4. TLAB 回收
TLAB 回收就是指线程将当前的 TLAB 丢弃回 Eden 区。TLAB 回收有两个时机:一个是之前提到的在分配对象时,剩余 TLAB 空间不足,在 TLAB 满但是浪费空间小于最大浪费空间的情况下,回收当前的 TLAB 并获取一个新的。另一个就是在发生 GC 时,其实更准确的说是在 GC 开始扫描时。不同的 GC 可能实现不一样,但是时机是基本一样的,这里以 G1 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); } //省略其他代码 }
为何要确保堆内存是可以解析的呢?这样有利于更快速的扫描堆上对象。确保内存可以解析里面做了什么呢?
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 其实就是将 ThreadLocalAllocBuffer 的堆内存指针 MarkWord 置为 NULL thread->tlab().retire(&stats); } else { //当前如果不回收,则将 TLAB 填充 Dummy Object 利于解析 thread->tlab().make_parsable(); } } } stats.publish(); }
TLAB 主要流程总结
JFR 对于 TLAB 的监控
根据上面的原理以及源代码分析,可以得知 TLAB 是 Eden 区的一部分,主要用于线程本地的对象分配。在 TLAB 满的时候分配对象内存,可能会发生两种处理:
- 线程获取新的 TLAB。老的 TLAB 回归 Eden,Eden进行管理,之后线程通过新的 TLAB 分配对象。
- 对象在 TLAB 外分配,也就 Eden 区。
对于 线程获取新的 TLAB 这种处理,也就是 refill,按照 TLAB 设计原理,这个是经常会发生的,每个 epoch 内可能会都会发生几次。但是对象直接在 Eden 区分配,是我们要避免的。JFR 对于
JFR 针对这两种处理有不同的事件可以监控。分别是jdk.ObjectAllocationOutsideTLAB
和jdk.ObjectAllocationInNewTLAB
。jdk.ObjectAllocationInNewTLAB
对应 refill,这个一般我们没有监控的必要(在你没有修改默认的 TLAB 参数的前提下),用这个测试并学习 TLAB 的意义比监控的意义更大。jdk.ObjectAllocationOutsideTLAB
对应对象直接在 Eden 区分配,是我们需要监控的。至于怎么不影响线上性能安全的监控,怎么查看并分析,怎么解决,以及测试生成这两个事件,会在下一节详细分析。
同时