《JVM G1源碼分析和調優》 —3.2 快速分配
3.2 快速分配
TLAB產生的目的就是為了進行內存快速分配。通常來說,JVM堆是所有線程的共享區域。因此,從JVM堆空間分配對象時,必須鎖定整個堆,以便不會被其他線程中斷和影響。為了解決這個問題,TLAB試圖通過為每個線程分配一個緩沖區來避免和減少使用鎖。
在分配線程對象時,從JVM堆中分配一個固定大小的內存區域并將其作為線程的私有緩沖區,這個緩沖區稱為TLAB。只有在為每個線程分配TLAB緩沖區時才需要鎖定整個JVM堆。由于TLAB是屬于線程的,不同的線程不共享TLAB,當我們嘗試分配一個對象時,優先從當前線程的TLAB中分配對象,不需要鎖,因此達到了快速分配的目的。
更進一步地講,實際上TLAB是Eden區域中的一塊內存,不同線程的TLAB都位于Eden區,所有的TLAB內存對所有的線程都是可見的,只不過每個線程有一個TLAB的數據結構,用于保存待分配內存區間的起始地址(start)和結束地址(end),在分配的時候只在這個區間做分配,從而達到無鎖分配,快速分配。
另外值得說明的是,雖然TLAB在分配對象空間的時候是無鎖分配,但是TLAB空間本身在分配的時候還是需要鎖的,G1中使用了CAS來并行分配。
圖3-2 TLAB在分區中的使用
在圖3-2中,Tn表示第n個線程,深灰色表示該TLAB塊已經分配完畢,淺灰色表示該TLAB塊還可以分配更多的對象。
從圖中我們可以看出,線程T1已經使用了兩個TLAB塊,T1、T2和T4的TLAB塊都有待分配的空間。這里并沒有提及Eden和多個分區的概念,實際上一個分區可能有多個TLAB塊,但是一個TLAB是不可能跨分區的。從圖中我們也可以看出,每個線程的TLAB塊并不重疊,所以線程之間對象的分配是可以并行的,且無影響。另外圖中還隱藏了一些細節:
T1已經使用完兩個TLAB塊,這兩個塊在回收的時候如何處理?
我們可以想象TLAB的大小是固定的,但是對象的大小并不固定,因此TLAB中可能存在內存碎片的問題,這個該如何解決?請繼續往下閱讀。
快速TLAB對象分配也有兩步:
從線程的TLAB分配空間,如果成功則返回。
不能分配,先嘗試分配一個新的TLAB,再分配對象。
代碼如下所示:
hotspot/src/share/vm/gc_interface/collectedHeap.inline.hpp
HeapWord* CollectedHeap::allocate_from_tlab(KlassHandle klass, Thread*
thread, size_t size) {
HeapWord* obj = thread->tlab().allocate(size);
if (obj != NULL)??? return obj;
// 省略一些判斷比如是否需要申請一個新的TLAB
return allocate_from_tlab_slow(klass, thread, size);
}
從TLAB已分配的緩沖區空間直接分配對象,也稱為指針碰撞法分配,其方法非常簡單,在TLAB中保存一個top指針用于標記當前對象分配的位置,如果剩余空間(end-top)大于待分配對象的空間(objSize),則直接修改top = top + ObjSize,相關代碼位于thread->tlab().allocate(size)中。對于分配失敗,處理稍微麻煩一些,相關代碼位于allocate_from_tlab_slow()中,在學習這部分代碼之前,先思考一下這樣的內存分配管理該如何設計。
如果TLAB過小,那么TLAB則不能存儲更多的對象,所以可能需要不斷地重新分配新的TLAB。但是如果TLAB過大,則可能導致內存碎片問題。假設TLAB大小為1M,Eden為200M。如果有40個線程,每個線程分配1個TLAB,TLAB被填滿之后,發生GC。假設TLAB中對象分配符合均勻分布,那么發生GC時,TLAB總的大小為:40×1×0.5 = 20M(Eden的10%左右),這意味著Eden還有很多空間時就發生了GC,這并不是我們想要的。最直觀的想法是增加TLAB的大小或者增加線程的個數,這樣TLAB在分配的時候效率會更高,但是在GC回收的時候則可能花費更長的時間。因此JVM提供了參數TLABSize用于控制TLAB的大小,如果我們設置了這個值,那么JVM就會使用這個值來初始化TLAB的大小。但是這樣設置不夠優雅,其實TLABSize默認值是0,也就是說JVM會推斷這個值多大更合適。采用的參數為TLABWasteTargetPercent,用于設置TLAB可占用的Eden空間的百分比,默認值1%,推斷方式為TLABSize = Eden×2×1%/線程個數(乘以2是因為假設其內存使用服從均勻分布),G1中是通過下面的公式計算的:
hotspot/src/share/vm/memory/threadLocalAllocBuffer.cpp
init_sz? = (Universe::heap()->tlab_capacity(myThread()) / HeapWordSize) /
(nof_threads * target_refills());
其中,tlab_capacity在G1CollectedHeap中實現,代碼如下所示:
hotspot/src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp
size_t G1CollectedHeap::tlab_capacity(Thread* ignored) const {
return (_g1_policy->young_list_target_length() - young_list()->survivor_
length()) * HeapRegion::GrainBytes;
}
簡單來說,tlab_capacity就是Eden所有可用的區域。另外要注意的是,這里采用的啟發式推斷也僅僅是一個近似值,實際上線程在使用內存分配對象時并不是無關的(不完全服從均勻分布),另外不同的線程類型對內存的使用也不同,比如一些調度線程、監控線程等幾乎不會分配新的對象。
在Java對象分配時,我們總希望它位于TLAB中,如果TLAB滿了之后,如何處理呢?前面提到TLAB其實就是Eden的一塊區域,在G1中就是HeapRegion的一塊空閑區域。所以TLAB滿了之后無須做額外的處理,直接保留這一部分空間,重新在Eden/堆分區中分配一塊空間給TLAB,然后再在TLAB分配具體的對象。但這里會有兩個小問題。
1.如何判斷TLAB滿了?
按照前面的例子TLAB是1M,當我們使用800K,還是900K,還是950K時被認為滿了?問題的答案是如何尋找最大的可能分配對象和減少內存碎片的平衡。實際上虛擬機內部會維護一個叫做refill_waste的值,當請求對象大于refill_waste時,會選擇在堆中分配,若小于該值,則會廢棄當前TLAB,新建TLAB來分配對象。這個閾值可以使用TLABRefillWasteFraction來調整,它表示TLAB中允許產生這種浪費的比例。默認值為64,即表示使用約為1/64的TLAB空間作為refill_waste,在我們的這個例子中,refill_waste的初始值為16K,即TLAB中還剩(1M - 16k = 1024 - 16 = 1008K)1008K內存時直接分配一個新的,否則盡量使用這個老的TLAB。
2.如何調整TLAB
如果要分配的內存大于TLAB剩余的空間則直接在Eden/HeapRegion中分配。那么這個1/64是否合適?會不會太小,比如通常分配的對象大多是20K,最后剩下16K,這樣導致每次都進入Eden/堆分區慢速分配中。所以,JVM還提供了一個參數TLAB
WasteIncrement(默認值為4個字)用于動態增加這個refill_waste的值。默認情況下,TLAB大小和refill_waste都會在運行時不斷調整,使系統的運行狀態達到最優。在動態調整的過程中,也不能無限制變更,所以JVM提供MinTLABSize(默認值2K)用于控制最小值,對于G1來說,由于大對象都不在新生代分區,所以TLAB也不能分配大對象,HeapRegion/2就會被認定為大對象,所以TLAB肯定不會超過HeapRegionSize
的一半。
如果想要禁用自動調整TLAB的大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB,
并使用-XX:TLABSize手工指定一個TLAB的大小。-XX:+PrintTLAB可以跟蹤TLAB的使用情況。一般不建議手工修改TLAB相關參數,推薦使用虛擬機默認行為。
繼續來看TLAB中的慢速分配,主要的步驟有:
TLAB的剩余空間是否太小,如果很小,即說明這個空間通常不滿足對象的分配,所以最好丟棄,丟棄的方法就是填充一個dummy對象,然后申請新的TLAB來分配對象。
如果不能丟棄,說明TLAB剩余空間并不小,能滿足很多對象的分配,所以不能丟棄這個TLAB,否則內存浪費很多,此時可以把對象分配到堆中,不使用TLAB分配,所以可以直接返回。
TLAB慢速分配代碼如下所示:
hotspot/src/share/vm/gc_interface/collectedHeap.cpp
HeapWord* CollectedHeap::allocate_from_tlab_slow(KlassHandle klass, Thread*
thread, size_t size) {
// 判斷TLAB尚未分配的剩余空間是否可以丟掉。如果剩余空間大于閾值則保留,其中閾值為
// refill waste limit,它由desired size和參數TLABRefillWasteFraction
// 計算得到
if (thread->tlab().free() > thread->tlab().refill_waste_limit()) {
// 不能丟掉,根據TLABWasteIncrement更新refill_waste的閾值
thread->tlab().record_slow_allocation(size);
// 返回NULL,說明在Eden/HeapRegion中分配
return NULL;
}
// 說明TLAB剩余空間很小了,所以要重新分配一個TLAB。老的TLAB不用處理,因為它屬于Eden,
// GC可以正確回收空間
size_t new_tlab_size = thread->tlab().compute_size(size);
// 分配之前先清理老的TLAB,其目的就是為了讓堆保持parsable可解析
thread->tlab().clear_before_allocation();
if (new_tlab_size == 0)???? return NULL;
// 分配一個新的TLAB...
HeapWord* obj = Universe::heap()->allocate_new_tlab(new_tlab_size);
if (obj == NULL)???? return NULL;
// 發生一個事件,用于統計分配信息
AllocTracer::send_allocation_in_new_tlab_event(klass, new_tlab_size *
HeapWordSize, size * HeapWordSize);
// 是否把內存空間清零
if (ZeroTLAB)? Copy::zero_to_words(obj, new_tlab_size);
// 分配對象,并設置TLAB的start、top、end等信息
thread->tlab().fill(obj, obj + size, new_tlab_size);
return obj;
}
為什么要對老的TLAB做清理動作?
TLAB存儲的都是已經分配的對象,為什么要清理以及清理什么?其實這里的清理就是把尚未分配的空間分配一個對象(通常是一個int[]),那么為什么要分配一個垃圾對象?代碼說明是為了棧解析(Heap Parsable),Heap Parsable是什么?為什么需要設置?下面繼續分析。
內存管理器(GC)在進行某些需要線性掃描堆里對象的操作時,比如,查看Heap
Region對象、并行標記等,需要知道堆里哪些地方有對象,而哪些地方是空白。對于對象,掃描之后可以直接跳過對象的長度,對于空白的地方只能一個字一個字地掃描,這會非常慢。所以可以把這塊空白的地方也分配一個dummy對象(啞元對象),這樣GC在線性遍歷時就能做到快速遍歷了。這樣的話就能統一處理,示例代碼如下:
HeapWord* cur = heap_start;
while (cur < heap_used) {
object o = (object)cur;
do_object(o);
cur = cur + o->size();
}
具體我們可以在新生代垃圾回收的時候再來驗證這一點。我們再看一下如何申請一個新的TLAB緩沖區,代碼如下所示:
hotspot/src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp
HeapWord* G1CollectedHeap::allocate_new_tlab(size_t word_size) {
return attempt_allocation(word_size, &dummy_gc_count_before, &dummy_
gclocker_retry_count);
}
它最終會調用到G1CollectedHeap中分配,其分配主要是在attempt_allocation完成的,步驟也分為兩步:快速無鎖分配和慢速分配。圖3-3為慢速分配流程圖。
TLAB緩沖區分配代碼如下所示:
hotspot/src/share/vm/gc_implementation/g1/g1CollectedHeap.inline.cpp
inline HeapWord* G1CollectedHeap::attempt_allocation(…) {
AllocationContext_t context = AllocationContext::current();
HeapWord* result = _allocator->mutator_alloc_region(context)->attempt_
allocation(word_size, false /* bot_updates */);
if (result == NULL) {
result = attempt_allocation_slow(…);
}
if (result != NULL)? dirty_young_block(result, word_size);
return result;
}
圖3-3 申請TLAB分區和對象慢速分配流程圖
快速無鎖分配:指的是在當前可以分配的堆分區中使用CAS來獲取一塊內存,如果成功則可以作為TLAB的空間。因為使用CAS可以并行分配,當然也有可能不成功。對于不成功則進行慢速分配,代碼如下所示:
hotspot/src/share/vm/gc_implementation/g1/heapRegion.inline.hpp
inline HeapWord* G1OffsetTableContigSpace::par_allocate_impl(size_t size,
HeapWord* const end_value) {
do {
HeapWord* obj = top();
if (pointer_delta(end_value, obj) >= size) {
HeapWord* new_top = obj + size;
HeapWord* result = (HeapWord*)Atomic::cmpxchg_ptr(new_top, top_addr(), obj);
if (result == obj)??? return obj;
} else {
return NULL;
}
} while (true);
}
對于不成功則進行慢速分配,慢速分配需要嘗試對Heap加鎖,擴展新生代區域或垃圾回收等處理后再分配。
首先嘗試對堆分區進行加鎖分配,成功則返回,在attempt_allocation_locked完成。
不成功,則判定是否可以對新生代分區進行擴展,如果可以擴展則擴展后再分配TLAB,成功則返回,在attempt_allocation_force完成。
不成功,判定是否可以進行垃圾回收,如果可以進行垃圾回收后再分配,成功則返回,在do_collection_pause完成。
不成功,如果嘗試分配次數達到閾值(默認值是2次)則返回失敗。
如果還可以繼續嘗試,再次判定是否進行快速分配,如果成功則返回。
不成功重新再嘗試一次,直到成功或者達到閾值失敗。
所以慢速分配要么成功分配,要么嘗試次數達到閾值后結束并返回NULL。代碼如下:
hotspot/src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp
HeapWord* G1CollectedHeap::attempt_allocation_slow(…) {
HeapWord* result = NULL;
for (int try_count = 1; /* we'll return */; try_count += 1) {
{
// 加鎖分配
result = _allocator->mutator_alloc_region(context)->attempt_
allocation_locked(word_size,??? false /* bot_updates */);
if (result != NULL)???????? return result;
if (GC_locker::is_active_and_needs_gc()) {
if (g1_policy()->can_expand_young_list()) {
result = _allocator->mutator_alloc_region(context)->attempt_
allocation_force(word_size, false /* bot_updates */);
if (result != NULL)???? return result;
}
should_try_gc = false;
} else {
if (GC_locker::needs_gc()) {
should_try_gc = false;
} else {
gc_count_before = total_collections();
should_try_gc = true;
}
}
}
if (should_try_gc) {
// GCLocker沒有進入臨界區,可以進行垃圾回收
result = do_collection_pause(word_size, gc_count_before, &succeeded,
GCCause::_g1_inc_collection_pause);
if (result != NULL)?????? return result;
if (succeeded) {
// 稍后可以進行回收,可以先返回
MutexLockerEx x(Heap_lock);
*gc_count_before_ret = total_collections();
return NULL;
}
} else {
// JNI進入臨界區中,判斷是否達到分配次數閾值
if (*gclocker_retry_count_ret > GCLockerRetryAllocationCount) {
MutexLockerEx x(Heap_lock);
*gc_count_before_ret = total_collections();
return NULL;
}
GC_locker::stall_until_clear();
(*gclocker_retry_count_ret) += 1;
}
// 可能因為其他線程正在分配或者GCLocker正在被競爭使用等,
// 在進行加鎖分配前再嘗試進行無鎖分配
result = _allocator->mutator_alloc_region(context)->attempt_
allocation(word_size, false /* bot_updates */);
if (result != NULL)????? return result;
}
ShouldNotReachHere();
return NULL;
}
這里GCLocker是與JNI相關的。簡單來說Java代碼可以和本地代碼交互,在訪問JNI代碼時,因為JNI代碼可能會進入臨界區,所以此時會阻止GC垃圾回收。這部分知識相對獨立,有關GCLocker的知識可以參看其他文章。
日志及解讀
從一個Java的例子出發,代碼如下:
public class Test {
private static final LinkedList
public static void main(String[] args) throws Exception {
int iteration = 0;
while (true) {
for (int i = 0; i < 100; i++) {
for (int j = 0; j < 10; j++) {
strings.add(new String("String " + j));
}
}
Thread.sleep(100);
}
}
}
通過命令設置參數,如下所示:
-Xmx128M -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
-XX:+PrintTLAB -XX:+UnlockExperimentalVMOptions -XX:G1LogLevel=finest
可以得到:
garbage-first heap?? total 131072K, used 37569K [0x00000000f8000000,
0x00000000f8100400, 0x0000000100000000)
region size 1024K, 24 young (24576K), 0 survivors (0K)
TLAB: gc thread: 0x0000000059ade800 [id: 16540] desired_size: 491KB slow
allocs: 8? refill waste: 7864B alloc: 0.99999??? 24576KB refills: 50
waste? 0.0% gc: 0B slow: 816B fast: 0Bd
對于多線程的情況,這里還會有每個線程的輸出結果以及一個總結信息。由于篇幅的關系此處都已經省略。下面我們分析日志中TLAB這個信息的每一個字段含義:
desired_size為期望分配的TLAB的大小,這個值就是我們前面提到如何計算TLABSize的方式。在這個例子中,第一次的時候,不知道會有多少線程,所以初始化為1,desired_size = 24576/50 = 491.5KB這個值是經過取整的。
slow allocs為發生慢速分配的次數,日志中顯示有8次分配到heap而沒有使用TLAB。
refill waste為retire一個TLAB的閾值。
alloc為該線程在堆分區分配的比例。
refills發生的次數,這里是50,表示從上一次GC到這次GC期間,一共retire過50個TLAB塊,在每一個TLAB塊retire的時候都會做一次refill把尚未使用的內存填充為dummy對象。
waste由3個部分組成:
gc:發生GC時還沒有使用的TLAB的空間。
slow:產生新的TLAB時,舊的TLAB浪費的空間,這里就是新生成50個TLAB,浪費了816個字節。
fast:指的是在C1中,發生TLAB retire(產生新的TLAB)時,舊的TLAB浪費的空間。
JVM
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。