今天,又是乾貨滿滿的一天。這是全網最硬核 JVM 系列的開篇,首先從 TLAB 開始。因爲文章很長,每一個人閱讀習慣不一樣,因此特此拆成單篇版和多篇版git
- 全網最硬核 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,裏面有不少你們常問的問題github
線程初始化的時候,若是 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,這裏不用關心
複製代碼
首先是 JVM 啓動的時候,全局 TLAB 須要初始化: src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
緩存
void ThreadLocalAllocBuffer::startup_initialization() {
//初始化,也就是歸零統計數據
ThreadLocalAllocStats::initialize();
// 假設平均下來,GC 掃描的時候,每一個線程當前的 TLAB 都有一半的內存被浪費,這個每一個線程使用內存的浪費的百分比率(也就是 TLABWasteTargetPercent),也就是等於(注意,僅最新的那個 TLAB 有浪費,以前 refill 退回的假設是沒有浪費的):1/2 * (每一個 epoch 內每一個線程指望 refill 次數) * 100
//那麼每一個 epoch 內每一個線程 refill 次數配置就等於 50 / TLABWasteTargetPercent, 默認也就是 50 次。
_target_refills = 100 / (2 * TLABWasteTargetPercent);
// 可是初始的 _target_refills 須要設置最多不超過 2 次來減小 VM 初始化時候 GC 的可能性
_target_refills = MAX2(_target_refills, 2U);
//若是 C2 JIT 編譯存在並啓用,則保留 CPU 緩存優化 Allocation Prefetch 空間,這個這裏先不用關心,會在別的章節講述
#ifdef COMPILER2
if (is_server_compilation_mode_vm()) {
int lines = MAX2(AllocatePrefetchLines, AllocateInstancePrefetchLines) + 2;
_reserve_for_allocation_prefetch = (AllocatePrefetchDistance + AllocatePrefetchStepSize * lines) /
(int)HeapWordSize;
}
#endif
// 初始化 main 線程的 TLAB
guarantee(Thread::current()->is_Java_thread(), "tlab initialization thread not Java thread");
Thread::current()->tlab().initialize();
log_develop_trace(gc, tlab)("TLAB min: " SIZE_FORMAT " initial: " SIZE_FORMAT " max: " SIZE_FORMAT,
min_size(), Thread::current()->tlab().initial_desired_size(), max_size());
}
複製代碼
每一個線程維護本身的 TLAB,同時每一個線程的 TLAB 大小不一。TLAB 的大小主要由 Eden 的大小,線程數量,還有線程的對象分配速率決定。 在 Java 線程開始運行時,會先分配 TLAB: src/hotspot/share/runtime/thread.cpp
安全
void JavaThread::run() {
// initialize thread-local alloc buffer related fields
this->initialize_tlab();
//剩餘代碼忽略
}
複製代碼
分配 TLAB 其實就是調用 ThreadLocalAllocBuffer 的 initialize 方法。 src/hotspot/share/runtime/thread.hpp
markdown
void initialize_tlab() {
//若是沒有經過 -XX:-UseTLAB 禁用 TLAB,則初始化TLAB
if (UseTLAB) {
tlab().initialize();
}
}
// Thread-Local Allocation Buffer (TLAB) support
ThreadLocalAllocBuffer& tlab() {
return _tlab;
}
ThreadLocalAllocBuffer _tlab;
複製代碼
ThreadLocalAllocBuffer 的 initialize 方法初始化 TLAB 的上面提到的咱們要關心的各類 field:src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
ide
void ThreadLocalAllocBuffer::initialize() {
//設置初始指針,因爲尚未從 Eden 分配內存,因此這裏都設置爲 NULL
initialize(NULL, // start
NULL, // top
NULL); // end
//計算初始指望大小,並設置
set_desired_size(initial_desired_size());
//全部 TLAB 總大小,不一樣的 GC 實現有不一樣的 TLAB 容量, 通常是 Eden 區大小
//例如 G1 GC,就是等於 (_policy->young_list_target_length() - _survivor.length()) * HeapRegion::GrainBytes,能夠理解爲年輕代減去Survivor區,也就是Eden區
size_t capacity = Universe::heap()->tlab_capacity(thread()) / HeapWordSize;
//計算這個線程的 TLAB 指望佔用全部 TLAB 整體大小比例
//TLAB 指望佔用大小也就是這個 TLAB 大小乘以指望 refill 的次數
float alloc_frac = desired_size() * target_refills() / (float) capacity;
//記錄下來,用於計算 EMA
_allocation_fraction.sample(alloc_frac);
//計算初始 refill 最大浪費空間,並設置
//如前面原理部分所述,初始大小就是 TLAB 的大小(_desired_size) / TLABRefillWasteFraction
set_refill_waste_limit(initial_refill_waste_limit());
//重置統計
reset_statistics();
}
複製代碼
src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp
oop
//計算初始大小
size_t ThreadLocalAllocBuffer::initial_desired_size() {
size_t init_sz = 0;
//若是經過 -XX:TLABSize 設置了 TLAB 大小,則用這個值做爲初始指望大小
//表示堆內存佔用大小都須要用佔用幾個 HeapWord 表示,因此用TLABSize / HeapWordSize
if (TLABSize > 0) {
init_sz = TLABSize / HeapWordSize;
} else {
//獲取當前epoch內線程數量指望,這個如以前所述經過 EMA 預測
unsigned int nof_threads = ThreadLocalAllocStats::allocating_threads_avg();
//不一樣的 GC 實現有不一樣的 TLAB 容量,Universe::heap()->tlab_capacity(thread()) 通常是 Eden 區大小
//例如 G1 GC,就是等於 (_policy->young_list_target_length() - _survivor.length()) * HeapRegion::GrainBytes,能夠理解爲年輕代減去Survivor區,也就是Eden區
//總體大小等於 Eden區大小/(當前 epcoh 內會分配對象指望線程個數 * 每一個 epoch 內每一個線程 refill 次數配置)
//target_refills已經在 JVM 初始化全部 TLAB 全局配置的時候初始化好了
init_sz = (Universe::heap()->tlab_capacity(thread()) / HeapWordSize) /
(nof_threads * target_refills());
//考慮對象對齊,得出最後的大小
init_sz = align_object_size(init_sz);
}
//保持大小在 min_size() 還有 max_size() 之間
//min_size主要由 MinTLABSize 決定
init_sz = MIN2(MAX2(init_sz, min_size()), max_size());
return init_sz;
}
//最小大小由 MinTLABSize 決定,須要表示爲 HeapWordSize,而且考慮對象對齊,最後的 alignment_reserve 是 dummy object 填充的對象頭大小(這裏先不考慮 JVM 的 CPU 緩存 prematch,咱們會在其餘章節詳細分析)。
static size_t min_size() {
return align_object_size(MinTLABSize / HeapWordSize) + alignment_reserve();
}
複製代碼
不一樣的 GC 方式,有不一樣的方式:post
G1 GC 中爲大對象(humongous object)大小,也就是 G1 region 大小的一半:src/hotspot/share/gc/g1/g1CollectedHeap.cpp
// For G1 TLABs should not contain humongous objects, so the maximum TLAB size
// must be equal to the humongous object limit.
size_t G1CollectedHeap::max_tlab_size() const {
return align_down(_humongous_object_threshold_in_words, MinObjAlignment);
}
複製代碼
ZGC 中爲頁大小的 8 分之一,相似的在大部分狀況下 Shenandoah GC 也是每一個 Region 大小的 8 分之一。他們都是指望至少有 8 分之 7 的區域是不用退回的減小選擇 Cset 的時候的掃描複雜度: src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp
MaxTLABSizeWords = MIN2(ShenandoahElasticTLAB ? RegionSizeWords : (RegionSizeWords / 8), HumongousThresholdWords);
複製代碼
src/hotspot/share/gc/z/zHeap.cpp
const size_t ZObjectSizeLimitSmall = ZPageSizeSmall / 8;
複製代碼
對於其餘的 GC,則是 int 數組的最大大小,這個和爲了填充 dummy object 表示 TLAB 的空區域有關。這個緣由以前已經說明了。
當 new 一個對象時,須要調用instanceOop InstanceKlass::allocate_instance(TRAPS)
src/hotspot/share/oops/instanceKlass.cpp
instanceOop InstanceKlass::allocate_instance(TRAPS) {
bool has_finalizer_flag = has_finalizer(); // Query before possible GC
int size = size_helper(); // Query before forming handle.
instanceOop i;
i = (instanceOop)Universe::heap()->obj_allocate(this, size, CHECK_NULL);
if (has_finalizer_flag && !RegisterFinalizersAtInit) {
i = register_finalizer(i, CHECK_NULL);
}
return i;
}
複製代碼
其核心就是heap()->obj_allocate(this, size, CHECK_NULL)
從堆上面分配內存: src/hotspot/share/gc/shared/collectedHeap.inline.hpp
inline oop CollectedHeap::obj_allocate(Klass* klass, int size, TRAPS) {
ObjAllocator allocator(klass, size, THREAD);
return allocator.allocate();
}
複製代碼
使用全局的 ObjAllocator
實現進行對象內存分配: src/hotspot/share/gc/shared/memAllocator.cpp
oop MemAllocator::allocate() const {
oop obj = NULL;
{
Allocation allocation(*this, &obj);
//分配堆內存,繼續看下面一個方法
HeapWord* mem = mem_allocate(allocation);
if (mem != NULL) {
obj = initialize(mem);
} else {
// The unhandled oop detector will poison local variable obj,
// so reset it to NULL if mem is NULL.
obj = NULL;
}
}
return obj;
}
HeapWord* MemAllocator::mem_allocate(Allocation& allocation) const {
//若是使用了 TLAB,則從 TLAB 分配,分配代碼繼續看下面一個方法
if (UseTLAB) {
HeapWord* result = allocate_inside_tlab(allocation);
if (result != NULL) {
return result;
}
}
//不然直接從 tlab 外分配
return allocate_outside_tlab(allocation);
}
HeapWord* MemAllocator::allocate_inside_tlab(Allocation& allocation) const {
assert(UseTLAB, "should use UseTLAB");
//從當前線程的 TLAB 分配內存,TLAB 快分配
HeapWord* mem = _thread->tlab().allocate(_word_size);
//若是沒有分配失敗則返回
if (mem != NULL) {
return mem;
}
//若是分配失敗則走 TLAB 慢分配,須要 refill 或者直接從 Eden 分配
return allocate_inside_tlab_slow(allocation);
}
複製代碼
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;
}
複製代碼
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;
}
複製代碼
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; }
複製代碼
從新計算會取 當前堆剩餘給 TLAB 可分配的空間 和 TLAB 指望大小 + 當前須要分配的空間大小 中的小的那個:
src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp
inline size_t ThreadLocalAllocBuffer::compute_size(size_t obj_size) {
//獲取當前堆剩餘給 TLAB 可分配的空間
const size_t available_size = Universe::heap()->unsafe_max_tlab_alloc(thread()) / HeapWordSize;
//取 TLAB 可分配的空間 和 TLAB 指望大小 + 當前須要分配的空間大小 以及 TLAB 最大大小中的小的那個
size_t new_tlab_size = MIN3(available_size, desired_size() + align_object_size(obj_size), max_size());
// 確保大小大於 dummy obj 對象頭
if (new_tlab_size < compute_min_size(obj_size)) {
log_trace(gc, tlab)("ThreadLocalAllocBuffer::compute_size(" SIZE_FORMAT ") returns failure",
obj_size);
return 0;
}
log_trace(gc, tlab)("ThreadLocalAllocBuffer::compute_size(" SIZE_FORMAT ") returns " SIZE_FORMAT,
obj_size, new_tlab_size);
return new_tlab_size;
}
複製代碼
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);
}
}
複製代碼
不一樣的 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();
}
複製代碼
不一樣的 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());
}
複製代碼