今天,又是干货满满的一天。这是全网最硬核 JVM 系列的开篇,首先从 TLAB 开始。由于文章很长,每个人阅读习惯不同,所以特此拆成单篇版和多篇版
- 全网最硬核 JVM TLAB 分析(单篇版不包含额外加菜)
- 全网最硬核 JVM TLAB 分析 1. 内存分配思想引入
- 全网最硬核 JVM TLAB 分析 2. TLAB生命周期与带来的问题思考
- 全网最硬核 JVM TLAB 分析 3. JVM EMA期望算法与TLAB相关JVM启动参数
- 全网最硬核 JVM TLAB 分析 4. TLAB 基本流程全分析
- 全网最硬核 JVM TLAB 分析 5. TLAB 源代码全解析
- 全网最硬核 JVM TLAB 分析 6. TLAB 相关热门Q&A汇总
- 全网最硬核 JVM TLAB 分析(额外加菜) 7. TLAB 相关 JVM 日志解析
- 全网最硬核 JVM TLAB 分析(额外加菜) 8. 通过 JFR 监控 TLAB
如果这里看的比较吃力,可以直接看第 10 章,热门 Q&A,里面有很多大家常问的问题
9.1. TLAB 类构成
线程初始化的时候,如果 JVM 启用了 TLAB(默认是启用的, 可以通过 -XX:-UseTLAB 关闭),则会初始化 TLAB。
TLAB 包括如下几个 field (HeapWord* 可以理解为堆中的内存地址): src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
1 | arduino复制代码//静态全局变量 |
9.2. TLAB 初始化
首先是 JVM 启动的时候,全局 TLAB 需要初始化: src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
1 | scss复制代码void ThreadLocalAllocBuffer::startup_initialization() { |
每个线程维护自己的 TLAB,同时每个线程的 TLAB 大小不一。TLAB 的大小主要由 Eden 的大小,线程数量,还有线程的对象分配速率决定。 在 Java 线程开始运行时,会先分配 TLAB: src/hotspot/share/runtime/thread.cpp
1 | javascript复制代码void JavaThread::run() { |
分配 TLAB 其实就是调用 ThreadLocalAllocBuffer 的 initialize 方法。 src/hotspot/share/runtime/thread.hpp
1 | scss复制代码void initialize_tlab() { |
ThreadLocalAllocBuffer 的 initialize 方法初始化 TLAB 的上面提到的我们要关心的各种 field:src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
1 | scss复制代码void ThreadLocalAllocBuffer::initialize() { |
9.2.1. 初始期望大小是如何计算的呢?
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
1 | scss复制代码//计算初始大小 |
9.2.2. TLAB 最大大小是怎样决定的呢?
不同的 GC 方式,有不同的方式:
G1 GC 中为大对象(humongous object)大小,也就是 G1 region 大小的一半:src/hotspot/share/gc/g1/g1CollectedHeap.cpp
1 | arduino复制代码// For G1 TLABs should not contain humongous objects, so the maximum TLAB size |
ZGC 中为页大小的 8 分之一,类似的在大部分情况下 Shenandoah GC 也是每个 Region 大小的 8 分之一。他们都是期望至少有 8 分之 7 的区域是不用退回的减少选择 Cset 的时候的扫描复杂度: src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp
1 | ini复制代码MaxTLABSizeWords = MIN2(ShenandoahElasticTLAB ? RegionSizeWords : (RegionSizeWords / 8), HumongousThresholdWords); |
src/hotspot/share/gc/z/zHeap.cpp
1 | ini复制代码const size_t ZObjectSizeLimitSmall = ZPageSizeSmall / 8; |
对于其他的 GC,则是 int 数组的最大大小,这个和为了填充 dummy object 表示 TLAB 的空区域有关。这个原因之前已经说明了。
9.3. TLAB 分配内存
当 new 一个对象时,需要调用instanceOop InstanceKlass::allocate_instance(TRAPS)
src/hotspot/share/oops/instanceKlass.cpp
1 | ini复制代码instanceOop InstanceKlass::allocate_instance(TRAPS) { |
其核心就是heap()->obj_allocate(this, size, CHECK_NULL)
从堆上面分配内存: src/hotspot/share/gc/shared/collectedHeap.inline.hpp
1 | arduino复制代码inline oop CollectedHeap::obj_allocate(Klass* klass, int size, TRAPS) { |
使用全局的 ObjAllocator
实现进行对象内存分配: src/hotspot/share/gc/shared/memAllocator.cpp
1 | scss复制代码oop MemAllocator::allocate() const { |
9.3.1. TLAB 快分配
src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp
1 | scss复制代码inline HeapWord* ThreadLocalAllocBuffer::allocate(size_t size) { |
9.3.2. TLAB 慢分配
src/hotspot/share/gc/shared/memAllocator.cpp
1 | arduino复制代码HeapWord* MemAllocator::allocate_inside_tlab_slow(Allocation& allocation) const { |
9.3.2.1 TLAB最大浪费空间
TLAB最大浪费空间 _refill_waste_limit
初始值为 TLAB 大小除以 TLABRefillWasteFraction:src/hotspot/share/gc/shared/threadLocalAllocBuffer.hpp
1 | scss复制代码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
1 | scss复制代码void ThreadLocalAllocBuffer::record_slow_allocation(size_t obj_size) { |
9.3.2.2. 重新计算 TLAB 大小
重新计算会取 当前堆剩余给 TLAB 可分配的空间 和 TLAB 期望大小 + 当前需要分配的空间大小 中的小的那个:
src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp
1 | scss复制代码inline size_t ThreadLocalAllocBuffer::compute_size(size_t obj_size) { |
9.3.2.3. 当前 TLAB 放回堆
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
1 | scss复制代码//在TLAB慢分配被调用,当前 TLAB 放回堆 |
9.4. GC 相关 TLAB 操作
9.4.1. GC 前
不同的 GC 可能实现不一样,但是 TLAB 操作的时机是基本一样的,这里以 G1 GC 为例,在真正 GC 前:
src/hotspot/share/gc/g1/g1CollectedHeap.cpp
1 | scss复制代码void G1CollectedHeap::gc_prologue(bool full) { |
为何要确保堆内存是可以解析的呢?这样有利于更快速的扫描堆上对象。确保内存可以解析里面做了什么呢?其实主要就是退还每个线程的 TLAB 以及填充 dummy object。
src/hotspot/share/gc/g1/g1CollectedHeap.cpp
1 | scss复制代码void CollectedHeap::ensure_parsability(bool retire_tlabs) { |
9.4.2. GC 后
不同的 GC 可能实现不一样,但是 TLAB 操作的时机是基本一样的,这里以 G1 GC 为例,在 GC 后:
src/hotspot/share/gc/g1/g1CollectedHeap.cpp
_desired_size
是什么时候变得呢?怎么变得呢?
1 | arduino复制代码void G1CollectedHeap::gc_epilogue(bool full) { |
src/hotspot/share/gc/shared/collectedHeap.cpp
1 | scss复制代码void CollectedHeap::resize_all_tlabs() { |
重新计算每个线程 TLAB 期望大小: src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
1 | scss复制代码void ThreadLocalAllocBuffer::resize() { |
本文转载自: 掘金