第3章
G1的對象配置設定
對象配置設定直接關系到記憶體的使用效率、垃圾回收的效率,不同的配置設定政策也會影響對象的配置設定速度,進而影響Mutator的運作。
本章主要介紹G1的對象配置設定是怎樣的。大體來說G1提供了兩種對象配置設定政策:基于線程本地配置設定緩沖區(Thread Local Allocation Buffer,TLAB)的快速配置設定和慢速配置設定;當不能成功配置設定對象時就會觸發垃圾回收,是以本章還總結了垃圾回收觸發的時機;最後介紹了對象配置設定過程中涉及的參數調優。值得注意的是本章介紹的内容不僅适用于G1的對象配置設定,大多數調優參數也适用于其他的垃圾回收器。
3.1 對象配置設定概述
為了提高效率,無論快速配置設定還是慢速配置設定,都應該在STW之外調用,即都應該盡量避免使用全局鎖,最好滿足不同Mutator之間能并行配置設定且無幹擾。但實際上堆空間隻有一個,是以JVM的設計者緻力于優秀的記憶體配置設定算法,把記憶體配置設定算法設計成幾個層次,首先進行無鎖配置設定,再進行加鎖,進而盡可能地滿足并行化配置設定。
我們以一個普通的Java對象配置設定為例,來梳理一下對象配置設定的過程。根據Java對象在JVM中的實作,JVM會先建立instanceklass,然後通過allocate_instance配置設定一個instanceOop。入口在InstanceKlass::allocate_instance,代碼如下:

在CollectedHeap::obj_allocate中完成記憶體配置設定,如果成功則初始化對象;如果不成功則抛出異常。主要工作在CollectedHeap::common_mem_allocate_noinit()中,我們直接來看這個函數。該函數包含了我們上面提到的兩種配置設定方法:TLAB快速配置設定allocate_from_tlab和慢速配置設定Universe::heap()->mem_allocate。代碼如下:
對象配置設定相對來說邏輯清晰,圖3-1為對象配置設定的全景流程圖。
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中,Tn表示第n個線程,深灰色表示該TLAB塊已經配置設定完畢,淺灰色表示該TLAB塊還可以配置設定更多的對象。
從圖中我們可以看出,線程T1已經使用了兩個TLAB塊,T1、T2和T4的TLAB塊都有待配置設定的空間。這裡并沒有提及Eden和多個分區的概念,實際上一個分區可能有多個TLAB塊,但是一個TLAB是不可能跨分區的。從圖中我們也可以看出,每個線程的TLAB塊并不重疊,是以線程之間對象的配置設定是可以并行的,且無影響。另外圖中還隐藏了一些細節:
□T1已經使用完兩個TLAB塊,這兩個塊在回收的時候如何處理?
□我們可以想象TLAB的大小是固定的,但是對象的大小并不固定,是以TLAB中可能存在記憶體碎片的問題,這個該如何解決?請繼續往下閱讀。
快速TLAB對象配置設定也有兩步:
□從線程的TLAB配置設定空間,如果成功則傳回。
□不能配置設定,先嘗試配置設定一個新的TLAB,再配置設定對象。
代碼如下所示:
從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中是通過下面的公式計算的:
簡單來說,tlab_capacity就是Eden所有可用的區域。另外要注意的是,這裡采用的啟發式推斷也僅僅是一個近似值,實際上線程在使用記憶體配置設定對象時并不是無關的(不完全服從均勻分布),另外不同的線程類型對記憶體的使用也不同,比如一些排程線程、監控線程等幾乎不會配置設定新的對象。
在Java對象配置設定時,我們總希望它位于TLAB中,如果TLAB滿了之後,如何處理呢?前面提到TLAB其實就是Eden的一塊區域,在G1中就是HeapRegion的一塊空閑區域。是以TLAB滿了之後無須做額外的處理,直接保留這一部分空間,重新在Eden/堆分區中配置設定一塊空間給TLAB,然後再在TLAB配置設定具體的對象。但這裡會有兩個小問題。
1.如何判斷TLAB滿了?
按照前面的例子TLAB是1M,當我們使用800K,還是900K,還是950K時被認為滿了?問題的答案是如何尋找最大的可能配置設定對象和減少記憶體碎片的平衡。實際上虛拟機内部會維護一個叫做ref?ill_waste的值,當請求對象大于ref?ill_waste時,會選擇在堆中配置設定,若小于該值,則會廢棄目前TLAB,建立TLAB來配置設定對象。這個門檻值可以使用TLABRef?illWasteFraction來調整,它表示TLAB中允許産生這種浪費的比例。預設值為64,即表示使用約為1/64的TLAB空間作為ref?ill_waste,在我們的這個例子中,ref?ill_waste的初始值為16K,即TLAB中還剩(1M - 16k = 1024 - 16 = 1008K)1008K記憶體時直接配置設定一個新的,否則盡量使用這個老的TLAB。
2.如何調整TLAB
如果要配置設定的記憶體大于TLAB剩餘的空間則直接在Eden/HeapRegion中配置設定。那麼這個1/64是否合适?會不會太小,比如通常配置設定的對象大多是20K,最後剩下16K,這樣導緻每次都進入Eden/堆分區慢速配置設定中。是以,JVM還提供了一個參數TLAB
WasteIncrement(預設值為4個字)用于動态增加這個ref?ill_waste的值。預設情況下,TLAB大小和ref?ill_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慢速配置設定代碼如下所示:
為什麼要對老的TLAB做清理動作?
TLAB存儲的都是已經配置設定的對象,為什麼要清理以及清理什麼?其實這裡的清理就是把尚未配置設定的空間配置設定一個對象(通常是一個int[]),那麼為什麼要配置設定一個垃圾對象?代碼說明是為了棧解析(Heap Parsable),Heap Parsable是什麼?為什麼需要設定?下面繼續分析。
記憶體管理器(GC)在進行某些需要線性掃描堆裡對象的操作時,比如,檢視Heap Region對象、并行标記等,需要知道堆裡哪些地方有對象,而哪些地方是空白。對于對象,掃描之後可以直接跳過對象的長度,對于空白的地方隻能一個字一個字地掃描,這會非常慢。是以可以把這塊空白的地方也配置設定一個dummy對象(啞元對象),這樣GC線上性周遊時就能做到快速周遊了。這樣的話就能統一處理,示例代碼如下:
具體我們可以在新生代垃圾回收的時候再來驗證這一點。我們再看一下如何申請一個新的TLAB緩沖區,代碼如下所示:
它最終會調用到G1CollectedHeap中配置設定,其配置設定主要是在attempt_allocation完成的,步驟也分為兩步:快速無鎖配置設定和慢速配置設定。圖3-3為慢速配置設定流程圖。
TLAB緩沖區配置設定代碼如下所示:
快速無鎖配置設定:指的是在目前可以配置設定的堆分區中使用CAS來擷取一塊記憶體,如果成功則可以作為TLAB的空間。因為使用CAS可以并行配置設定,當然也有可能不成功。對于不成功則進行慢速配置設定,代碼如下所示:
對于不成功則進行慢速配置設定,慢速配置設定需要嘗試對Heap加鎖,擴充新生代區域或垃圾回收等處理後再配置設定。
□首先嘗試對堆分區進行加鎖配置設定,成功則傳回,在attempt_allocation_locked完成。
□不成功,則判定是否可以對新生代分區進行擴充,如果可以擴充則擴充後再配置設定TLAB,成功則傳回,在attempt_allocation_force完成。
□不成功,判定是否可以進行垃圾回收,如果可以進行垃圾回收後再配置設定,成功則傳回,在do_collection_pause完成。
□不成功,如果嘗試配置設定次數達到門檻值(預設值是2次)則傳回失敗。
□如果還可以繼續嘗試,再次判定是否進行快速配置設定,如果成功則傳回。
□不成功重新再嘗試一次,直到成功或者達到門檻值失敗。
是以慢速配置設定要麼成功配置設定,要麼嘗試次數達到門檻值後結束并傳回NULL。代碼如下:
這裡GCLocker是與JNI相關的。簡單來說Java代碼可以和本地代碼互動,在通路JNI代碼時,因為JNI代碼可能會進入臨界區,是以此時會阻止GC垃圾回收。這部分知識相對獨立,有關GCLocker的知識可以參看其他文章。
日志及解讀
從一個Java的例子出發,代碼如下:
通過指令設定參數,如下所示:
-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 ref?ill waste: 7864B alloc: 0.99999 24576KB ref?ills: 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的時候都會做一次ref?ill把尚未使用的記憶體填充為dummy對象。
□waste由3個部分組成:
- gc:發生GC時還沒有使用的TLAB的空間。
- slow:産生新的TLAB時,舊的TLAB浪費的空間,這裡就是新生成50個TLAB,浪費了816個位元組。
- fast:指的是在C1中,發生TLAB retire(産生新的TLAB)時,舊的TLAB浪費的空間。
3.3 慢速配置設定
當不能進行快速配置設定,就進入到慢速配置設定。實際上在TLAB中也有可能進入到慢速配置設定,就是我們前面提到的attempt_allocation,前面已經解釋過。
這裡的慢速配置設定是指在TLAB中經過努力配置設定還不能成功,再次進入慢速配置設定,我們來看一下這個更慢的慢速配置設定:
□attempt_allocation嘗試進行對象配置設定,如果成功則傳回。值得注意的是在attempt_
allocation裡面可能會進行垃圾回收,這裡的垃圾回收是指增量的垃圾回收,主要是新生代或者混合收集,關于收集的内容将在下面的章節介紹,配置設定相關的代碼在3.2節已經介紹過了,不再贅述。
□如果大對象在attempt_allocation_humongous,直接配置設定的老生代。
□如果配置設定不成功,則進行GC垃圾回收,注意這裡的回收主要是Full GC,然後再配置設定。因為這裡是配置設定的最後一步,是以進行幾次不同的垃圾回收和嘗試。主要代碼在satisfy_failed_allocation中。
□最終成功配置設定或者失敗達到一定次數,則配置設定失敗。
慢速配置設定代碼如下所示:
3.3.1 大對象配置設定
大對象配置設定和TLAB中的慢速配置設定基本類似。唯一的差別就是對象大小不同。步驟主要:
□嘗試垃圾回收,這裡主要是增量回收,同時啟動并發标記。
□嘗試開始配置設定對象,對于大對象分為兩類,一類是大于HeapRegionSize的一半,但是小于HeapRegionSize,即一個完整的堆分區可以儲存,則直接從空閑清單直接拿一個堆分區,或者配置設定一個新的堆分區。如果是連續對象,則需要多個堆分區,思路同上,但是處理的時候需要加鎖。
□如果失敗再次嘗試垃圾回收,之後再配置設定。
大對象配置設定代碼如下所示:
3.3.2 最後的配置設定嘗試
先嘗試配置設定一下,因為并發之後可能可以配置設定:
□嘗試擴充新的分區,成功則傳回。
□不成功進行Full GC,但是不回收軟引用,再次配置設定成功則傳回。
□不成功進行Full GC,回收軟引用,最後一次配置設定成功則傳回;不成功傳回NULL,即配置設定失敗。
最後嘗試配置設定代碼如下所示:
3.4 G1垃圾回收的時機
通常來說,在配置設定對象時如果記憶體不足,就會觸發垃圾回收,G1提供了3種垃圾回收的算法,分别是新生代回收、混合回收和Full GC,是以在記憶體配置設定的地方可以看到這3種收集算法。
總結來看,回收發生在兩個時機:第一,在配置設定記憶體時發現記憶體不足,進入垃圾回收;第二,外部顯式地調用回收的方法,如在Java代碼中調用system.gc()進入回收。不同的回收時機選擇的回收方式也不同。
3.4.1 配置設定時發生回收
前面提到快速配置設定和慢速配置設定在記憶體不足時,都有可能發生垃圾回收,回收之後再繼續配置設定。在配置設定時将涉及3種回收算法,前面已經介紹此處不再贅述。
3.4.2 外部調用的回收
常見有兩種外部調用情況可以激活垃圾回收:
□外部顯式調用system.gc觸發。一般來說,如果我們沒有設定DisableExplicitGC(預設為false),表示可以接受這個函數顯式地觸發GC。這個時候觸發的GC都是Full GC,但是如果設定了ExplicitGCInvokesConcurrent,則表示可以進行并發的混合回收。
□如果和JNI互動,JNI代碼進入了臨界區(比如JNI代碼為了優化性能,提供了一個函數jni_GetPrimitiveArrayCritical/jni_GetStringCritical用于直接通路原始記憶體資料,但是為了保證安全必須使用GCLocker進行加鎖。當加鎖後發生了GC請求,此時GC會被延遲,直到GCLocker執行了unlock會重新補一個GC),而且設定了GCLockerInvokesConcurrent,則可以進行并發混合回收,如果沒有設定則可能啟動新生代回收。
實際上JVM還提供了WhiteBox API用于JVM内部測試,也可以執行GC,是以也會觸發新生代回收、FGC等。
3.5 參數介紹和調優
本章詳細介紹了G1中對象的快速配置設定和慢速配置設定,其中快速配置設定和TLAB相關。本節給出實際應用中對象配置設定用到的相關參數和一些個人經驗,如下所示:
□在優化調試TLAB的時候,在調試環境中可以通過打開PrintTLAB來觀察TLAB配置設定和使用的情況。
□參數UseTLAB,指是否使用TLAB。大量的實驗可以證明使用TLAB能夠加速對象配置設定;該參數預設是打開的,不要關閉它。
□參數ResizeTLAB,指是否允許TLAB大小動态調整。前面提到TLAB會進行動态化調整,主要是基于曆史資訊(配置設定大小、線程數等),有基準測試表明使用動态調整TLAB大小效率更高。
□參數MinTLABSize,指設定TLAB的最小值。實際應用需要設定該值,比如64K,一般可以根據情況設定和調整該值。
□參數TLABSize,指設定TLAB的大小。實際中不要設定TLABSize,設定之後TLAB就不能動态調整了,即會使用一個固定大小的TLAB,前面我們提到GC可以根據情況動态調整TLAB,在配置設定效率和記憶體碎片之間找到一個平衡點,如果設定該值則這種平衡就失效了。
□參數TLABWasteTargetPercent,指的是TLAB可占用的Eden空間的百分比,預設值是1。可以根據情況調整TLABWasteTargetPercent,增大則可以配置設定更多的TLAB,3.1節中給出了具體的計算方式;另外如果實際中線程數目很多,建議增大該值,這樣每個線程的TLAB不至于太小。
□參數TLABRef?illWasteFraction,指的是TLAB中浪費空間和TLAB塊的比例,預設值是64。可以根據情況調整TLABRef?illWasteFraction,主要考量點是記憶體碎片和配置設定效率的平衡,如果發現日志waste中的slow和fast很大,說明浪費嚴重,可以适當減少該參數值。
□參數TLABWasteIncrement,指的是動态的增加浪費空間的位元組數,預設值是4。增加該值會增加TLAB浪費的空間;一般不用設定。
□參數GCLockerRetryAllocationCount預設值為2,表示當配置設定中的垃圾回收次數超過這個門檻值之後則直接失敗。
最後再強調一點,TLAB不是G1才引入的,對象配置設定是JVM提供的基礎配置設定功能,隻不過G1結合自己記憶體分區的特征,以及垃圾回收的具體實作,重新實作了配置設定的政策,重用了這些參數的功能和使用方法,且沒有引入額外的參數,是以這一部分内容不僅适用于G1的調優,其他的垃圾回收器同樣适用。