深刻學習 G1回收器和JVM: G1的對象分配(3)

對象分配概述

  • G1提供了兩種分配策略java

    • 基於線程本地分配緩衝區(Thread Local Allocation Buffer,TLAB)的快速分配
    • 基於TLAB的慢速分配
    • 慢速分配
  • 當不能成功分配對象時就會觸發垃圾回收

image.png

對象分配流程圖

線程本地分配緩衝區(Thread Local Allocation Buffer,TLAB)

快速分配

  • TLAB的產生就是爲了快速分配內存
  • JVM堆是全部線程的共享區域,因此,從JVM堆空間分配對象時,必須鎖定整個堆,以便不會被其餘線程中斷和影響。
  • TLAB就是爲每一個線程都分配的一個獨立的緩衝區,這樣就能夠減小使用鎖的頻率,只有在爲每一個線程分配TLAB的時候,才須要鎖定整個JVM堆
  • TLAB屬於Eden區域中的內存,不一樣線程的TLAB都位於Eden區,Eden區對全部的線程都是可見的。每一個線程的TLAB有內存區間,在分配的時候只在這個區間分配。
  • JVM分配TLAB的時候用CAS分配
  • JVM快速分配TLAB對象流程安全

    1. 從線程的TLAB分配空間,若是成功則返回。
    2. 若是分配失敗,則嘗試先分配一個新的TLAB,再分配對象
  • 若是TLAB太小,那麼TLAB不能儲存更多的對象,可能須要不斷地給整個堆上鎖從新分配新的TLAB。
  • 若是TLAB過大,那麼容易致使更多的內存碎片,內存使用效率不高。更容易發生GC。
  • JVM提供了參數 TLABSize來控制TLAB的大小,默認爲0,JVM會自動推斷這個值多大合適。
  • 參數TLABWasteTargetPercent,用於設置TLAB佔Eden空間的百分比默認值1%併發

    • 推斷方式:TLABSize = Eden 2 1% / 線程個數(乘以2是假設內存使用服從均勻分佈)

指針碰撞法分配

  1. 若是TLAB剩餘空間(end - top) 大於當前對象待分配空間。則直接修改 top = top + objSize(對象大小);
  2. 若是TLAB滿了,則會保留這一部分空間,從新從堆內存中劃一片空間給TLAB

如何判斷TLAB滿了

  • 虛擬機內部會維護一個 refill_waste,當請求對象大於refill_waste,會選擇在堆中分配,若小於該值,廢棄當前的TLAB,新建一個TLAB來分配該對象。
  • refill_waste 可使用 TLABRefillWasteFraction 參數來調整,默認值爲64,即表示使用1/64的TLAB空間做爲refill_waste
  • 假設 TLAB爲1M,那 refill_waste 爲16k,即當TLAB使用了1008k時(1024k - 16k),就直接分配一個新的,不然就儘可能使用這個老的TLAB

如何調整TLAB

  • 除了 TLABRefillWasteFraction ,JVM還提供了參數TLAB WasteIncrement(默認值爲4個字),用於動態增長refill_waste
  • refill_waste和TLAB的大小都會不斷動態調整,使系統狀態達到最優。
  • 因爲大對象都不會在新生代中,TLAB都不能分配大對象,因此TLAB的大小不會大於HRSize/2。(大於HRSize/2就會被認爲是大對象)

TLAB中的慢速分配

  • 若是TLAB中的剩餘空間很小(TLAB滿了),說明這個空間一般不知足對象分配,能夠直接丟棄,填充一個dummy對象,而後申請一個新的TLAB來分配對象。
  • 若是TLAB剩餘空間比較多,那就不能丟棄TLAB,這時候就直接將對象分配到堆中,不使用TLAB,直接返回。

清理老的TLAB(dummy對象填充)

  • GC在線性掃描堆的時候(好比查看HeapRegion對象,並行標記等),要知道哪裏有對象,哪裏是空白的。對於對象,掃描以後,能夠直接跳過這個對象的長度,若是是空白的,就須要一個字一個字的掃,效率很低。因此把TLAB的空白地方分配一個dummy對象(啞元對象),這樣GC就能作到快速遍歷了。

申請一個新的TLAB緩衝區

調用 G1CollectHeap中分配,主要是在 attempt_allocation中完成
  • 快速無鎖分配:在當前能夠分配的堆空間內,經過CAS來獲取一塊內存,若是成功,就能夠做爲TLAB的空間。
  • 由於使用CAS,因此也有可能不成功。
  • 不成功則進行慢速分配。
  • 慢速分配須要嘗試對Heap加鎖,拓展新生代區域或垃圾回收等處理後再分配。函數

    1. 首先嚐試對堆分區進行加鎖後進行分配,成功則返回。
    2. 若是不成功,則斷定是否能夠對新生代分區進行拓展。若是能夠拓展,則拓展之後再分配TLAB,成功則返回。
    3. 若是不成功,則斷定是否能夠進行垃圾回收,若是能夠進行,垃圾回收之後再分配,成功則返回。
    4. 若是不成功,則再次嘗試,若是嘗試次數達到閾值(默認2),則返回失敗(NULL)。(若是能夠繼續嘗試,則從快速分配開始重頭嘗試)

日誌及解讀

經過命令設置參數,以下所示:性能

-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浪費的空間。

慢速分配

這裏的慢速分配指的是在TLAB中分配,不能成功,最後進入慢速分配。(比TLAB慢速分配更慢
  • attempt_allocation嘗試進行對象分配,若是成功則返回。
  • 若是大對象,在attempt_allocatin_humongous分配,直接分配老年代.
  • 若是分配不成功,則進行GC垃圾回收(主要是FullGC),而後再分配。
  • 最終成功,或者嘗試N次後失敗,則分配失敗。

大對象分配流程(和TLAB相似,惟一區別就是對象大小不一樣)

  • 嘗試垃圾回收(主要是增量回收,同時啓動併發標記)
  • 嘗試開始分配對象spa

    • 若是大於HRSize的一半且小於HRSize(即一個完整分區能夠保存):直接從空閒列表得到一個分區,或者分配一個新分區。
    • 若是是大於HRSize,則是個連續對象,須要多個分區,思路同上,可是須要加鎖。
  • 若是失敗再從嘗試垃圾回收開始。若是失敗達到必定次數,則分配失敗。

最後的分配嘗試

  • 嘗試拓展新的分區,成功則返回
  • 不成功進行則進行FullGC,可是不回收軟引用,再次分配成功則返回
  • 不成功再次進行FullGC,回收軟引用,成功則返回
  • 不成功則返回Null,分配失敗

G1垃圾回收的時機

  • 分配內存時發現內存不足
  • 外部顯式調用線程

    • java代碼中的system.gc()指針

      • 若是設置了DisableExplicitGC(默認爲false),則不接受這個函數顯式觸發GC
      • 默認爲FullGC,若是設置了ExplicitGCInvokesConcurrent,表示能夠進行併發的混合回收
    • 和JNI交互,JNI代碼進入了臨界區

      • 如JNI代碼爲了優化性能,提供了一個函數jni_GetPrimitiveArrayCritical/jni_GetStringCritical用於直接訪問原始內存數據,可是爲了保證安全必須使用GCLocker進行加鎖。當加鎖後發生了GC請求,此時GC會被延遲,直到GCLocker執行了unlock會從新補一個GC
      • 若是設置了ExplicitGCInvokesConcurrent,表示能夠進行併發的混合回收,若是沒有設置,可能啓動新生代回收

參數介紹和調優

  • 在優化調試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不至於過小。
  • 參數TLABRefillWasteFraction,指的是TLAB中浪費空間和TLAB塊的比例,默認值是64。能夠根據狀況調整TLABRef?illWasteFraction,主要考量點是內存碎片和分配效率的平衡,若是發現日誌waste中的slow和fast很大,說明浪費嚴重,能夠適當減小該參數值
  • 參數TLABWasteIncrement,指的是動態的增長浪費空間的字節數,默認值是4。增長該值會增長TLAB浪費的空間;通常不用設置
  • 參數GCLockerRetryAllocationCount默認值爲2,表示當分配中的垃圾回收次數超過這個閾值以後則直接失敗。
TLAB不是G1才引入的,對象分配是JVM提供的基礎分配功能,只不過G1結合本身內存分區的特徵,以及垃圾回收的具體實現,從新實現了分配的策略,重用了這些參數的功能和使用方法,且沒有引入額外的參數,因此這一部份內容不只適用於G1的調優,其餘的垃圾回收器一樣適用。
相關文章
相關標籤/搜索