深刻學習 G1回收器和JVM:新生代回收(5)

新生代回收(YOUNG GC,YGC)

在內存分配的時候,剩餘空間不能知足要分配的對象時,就會優先觸發YGC。
G1每次回收的內存和分區個數可能並不相同,可是每一次YGC都收集全部的新生代分區,因此每次YGC以後都會調整新生代分區的數目。

YGC算法概述

YGC的執行順序

  1. 進行收集以前須要STW
  2. 選擇要收集的CSet,對於YGC來講,整個新生代分區都是CSet
  3. 進入並行處理任務java

    • 根掃描並處理:處理過程會把根直接引用的對象複製到新的Survivor區,而後把被引用的field入棧等待後續複製處理。
    • 處理老年代分區到新生代分區的引用:先更新RSet,而後從RSet出發,把RSet所在卡表對應的分區內存塊中全部的對象都認爲是根,把這些根引用的對象複製到新的Survivor區,而後把被引用對象的field入棧等待後續的複製處理。
    • JIT代碼掃描:根據棧中的對象,深度遞歸遍歷複製對象。
  4. 其餘任務處理(大部分是串行處理)算法

    • JIT代碼位置更新::更新相關指針所指向的位置。
    • 引用處理:把引用中使用的存活對象複製到新的分區。
    • 字符串去重優化回收:G1的新功能,優化字符串的使用效率。
    • 清除卡表:把全局卡表中已經處理過的分區對應的卡表清空。
    • JIT代碼回收:代碼已經能夠回收,其實是刪除相關引用。
    • Evac失敗處理:若是Evac失敗處理,則進行處理,主要是恢復對象頭。
    • 引用再處理:把引用中還活着的對象放入引用隊列。
    • redirty:主要就是重構RSet
    • 釋放CSet:啓動釋放內存,把這些分區放入自由列表(Free List),若是對象分配時須要新的分區,能夠從自由列表中獲取。
    • 嘗試回收大對象:判斷這些大對象分區是否有RSet引用,只須要判斷大對象所在第一個分區,若是沒有引用則說明整個大對象都死亡了。
    • 嘗試拓展內存:根據GCTImeRatio和G1ExpandByPercentOfAvailable來判讀是否可拓展,若是能夠,拓展多大的內存。
    • 調整新生代分區的數目:調整Refinement Zone的閾值等。主要更具GC的執行時間和目標停頓時間預測下次可能發生垃圾回收時能接受的最大的分區數。
總體流程

img

YGC代碼分析

並行任務

並行任務是經過 FlexibleWorkGang來執行 G1ParTask
  1. 根掃描並處理,針對全部的根,對可達對象作:數組

    1. 若是對象沒設置過標記信息,把對象從Eden複製到Survivor,而後針對對象每一個field緩存

      1. 若是field所引用的分區在CSet,則把對象的地址加入到G1ParScanTreadStates(PSS)的隊列中待掃描。
      2. 若是字段不在CSet,則更新所在堆分區的RSet
    2. 更新根對象到對象新的位置:併發

      1. 當發現對象須要被複制,先複製對象到新的位置
      2. 複製以後把老位置的引用對象頭標記爲11,而後把老對象頭裏的指針指向新位置的引用。
  2. 處理老年代分區到新生代分區的引用jvm

    1. 處理Dirty Card,更新RSet,更新老年代分區到新生代分區的引用。
    2. 掃描RSet,把引用者做爲根,從根出發,堆可達對象進行根掃描並處理。
  3. 複製。在PSS隊列中的對象都是活躍對象,每個對象都要複製到Survivor區,而後針對該對象的每個字段:性能

    1. 若是字段所引用的分區在CSet,則把對象的地址加到PSS的隊列中待掃描;
    2. 循環1 直到沒有對象。

根處理

JVM的根在這裏也稱爲強根,指的是JVM的堆外空間引用到堆空間的對象,有棧或者全局變量等。整個根分爲兩大類:
  • java根:主要指類加載器和線程棧。測試

    • 類加載器主要是遍歷這個類加載器中全部存活的Klass(指jvm對java對象的元數據描述)並複製到Survivor或者晉升到老年代。
    • 線程棧既會處理普通的java線程棧分配的局部變量,也會處理本地方法棧訪問的堆對象。
    • jvm根:一般指全局變量

RSet處理

RSet處理的人口在 G1RootProcessor::scan_remembered_sets
  • 更新RSet就是把引用關係存儲到RSet對應的PRT中。
  • 掃描RSet則是根據RSet的存儲信息掃描找到對應的引用者(即根)
  • 由於RSet內部有3種不一樣粒度的存儲類型,因此根的大小也會不一樣。

更新RSet

Refine線程處理綠,黃,紅區。白區則由YGC來處理,處理方式與Refine線程同樣。優化

掃描RSet:(每一個GC線程都只會針對部分的分區處理,因此它們之間能並行運行)

掃描RSet會處理CSet中全部待回收的分區。先找到RSet中的老年代分區對象,這些對象指向CSet中的對象。而後對這些老年代對象處理,把老年代對象field指向的對象的地址放入隊列中待後續處理。spa

引用者分區處理

找到卡表所在的區域,RSet中存儲的是對象起始地址所對應的卡表地址,因此必定能找到對象。

img

RSet裏面的每個PRT存儲的就是對應卡表的位置(即指針)。

在圖中咱們假設對象一、二、3分配連續圖中第2個卡表所指向的內存。因爲卡表是按照512字節對齊,因此對象一、二、3的卡表指針是相同的。

當對象一、二、3之一引用到新生代的對象時,在新生代裏面的PRT都只能找到圖中第2個卡表的起始位置。而這個卡表指針不能明確地說明是對象一、2或者3,因此當經過RSet找引用者的時候,這個指針只能理解爲對象一、二、3均可能引用到新生代了。

針對這個狀況,要找到準確的引用者,必須有如下兩步
  1. 先找到對象1的起始位置。G1經過使用G1BlockOffsetTable來記標記對象所在塊的起始位置。
  2. 遍歷從第一個對象到最後一個對象爲止,查找對象1,2,3全部的field是否都有到待回收分區的引用,若是有,說明該field是一個有效的引用,把該field放入待處理隊列用於後續的遍歷和複製。

複製

複製Evac處理:實際上就是將在java根和RSet根找到的子對象所有複製到新的分區中。若是一切順利,在CSet中全部活躍的對象都將被複制到新的分區中,而且在複製的過程當中,引用關係也隨之處理。若是發生了失敗,處理流程也基本相似。

GC如何進行並行處理

  • 對於java根處理來講,根對象有多個,因此分配一個數組來存儲各個並行任務的狀態,在使用的時候多個線程經過CAS來獲取數組中的元素來保證並行執行任務。
  • 對於RSet根來講,處理的時候是根據分區來進行處理。即便老年代對象中引用了多個CSet中不一樣的分區,也沒問題,由於這時候僅僅是標記出對象,即便一個對象被處理屢次也沒問題。
  • 在Evac中,由於每次處理對象的時候,須要對對象進行復制,這個時候是須要多個線程使用CAS來保證串行,先把對象標記爲待回收,以後才能複製。即只能由一個線程複製成功,其餘線程都會重用這個新對象的複製。

    • Evac因爲設計到對象的複製,這個很是耗時,因此在這個階段還提供了任務竊取功能。在併發執行的過程當中,GC線程優先處理本地的隊列。當本地的隊列沒有任務的時候,竊取其餘隊列的任務,幫助別的隊列。由於Evac保證了並行執行時的衝突問題,因此從別的對象隊列中取幾個待處理對象直接處理便可。

其餘處理

其餘處理大部分是串行處理,而且大可能是在並行工做結束後開始,大可能是由於處理過程須要同步等待,須要獨佔訪問臨界區。(除了入Redirty,字符串去重和引用)

參數調優

  • 參數ParallelGCThreads,默認值爲0,表示的是並行執行GC的線程個數。G1能夠根據CPU的個數自行推斷線程數;GC是CPU密集型的任務,一般來講線程個數不該該超過CPU核數通常不用設置該值
  • 在對新生代收集的過程當中,若是對象在YGC發生了必定次數以後還存活,這意味着對象有很大的機率存活更長的時間,因此一般會把它晉升到老生代。而這個次數能夠經過參數MaxTenuringThreshold控制,默認值是15,即發生15次YGC後,對象仍然存活,存活的對象會晉升到老生代。這個值最大隻能是15。減少該值能夠會把對象更早地提高到老生代。
  • 參數G1RsetScanBlockSize,默認值爲64,指掃描Rset時一次處理的量,其目的是爲了加速處理速度;若是計算能力較強,能夠增大該值
  • 參數SurvivorRatio,默認值爲8,指Eden和一個Survivor分區之間的比例;減少該值,將致使Survivor分區大小變大,G1中並不會由於增大該值直接致使Eden變小,Eden是根據GC的時間來預測的
  • 參數TargetSurvivorRatio默認值爲50,表示指望Survivor的大小。增大該值,則用於下一次Survivor的空間變大,晉升到Old分區的機率會減小。
  • 參數ParGCArrayScanChunk,默認值爲50,表示當一個對象數組的長度超過這個閾值以後,不會一次性遍歷它,而是分屢次處理,每次的長度都是這個閾值,只有最後一次處理的長度在ParGCArrayScanChunk和2×ParGCArrayScanChunk之間。減少該值會減小棧溢出的狀況,增大該值效率會略有提高。G1中的處理和其餘的收集器略有不一樣,其餘的收集器中當使用對象壓縮指針,而且發生Evac失敗時可能致使信息丟失,因此若是你在使用其餘的收集器,當發生這種問題時,能夠XX:UseCompressedOops或者把ParGCArrayScanChunk設置成最大的對象數組長度,即永遠都不要對對象數組分屢次處理。
  • 參數ResizePLAB,默認值爲true,表示在垃圾回收結束後會根據內存的使用狀況來調整PLAB的大小,可是目前G1中的GC線程在不一樣的階段如Evac,引用處理等都會涉及內存分配,因此在PLAB的調整上是根據總體內存的使用狀況進行的,這個成本比較高。所以在一些基準測試中發現禁止該選項可能有更好的效果,但這並不必定也適用於你的應用,關於PLAB效率和性能有一個bug,若是使用該選項也能夠進行調整並測試。關於PLAB在JDK9等後面的版本中會引入相關參數。
  • 參數YoungPLABSize默認值爲4096,是新生代PLAB緩存大小。在32位JVM中PLAB爲16KB,在64位JVM中爲32KB,表示對象從Eden複製到Survivor時,每次請求16KB做爲分配緩存,提升分配效率。增大該值能夠提升分配的效率,可是可能增長內存碎片,同時可能使得S分區很快耗盡;實際調優中能夠嘗試先減少該值。
  • 參數OldPLABSize,默認值爲1024,指老生代PLAB緩存大小。在32位JVM中PLAB爲4KB,64位JVM中爲8KB,表示對象從Eden複製到Old時,每次請求4KB做爲分配緩存,提升分配效率。增大該值能夠提升分配的效率,可是可能增長內存碎片;一般來講Old分區空間更大,實際調優中能夠嘗試先增大該值。
  • 參數ParallelGCBufferWastePct,默認值爲10,表示對象從Eden到Survivor或者Old區的時候,若是剩餘空間小於這個比例,且不能分配新對象時能夠丟棄這個PLAB塊,申請一個新的PLAB,因此這個值越大分配的效率越高,內存浪費也越嚴重;這個參數和TLABRefillWasteFraction相似。
  • 參數G1EagerReclaimHumongousObjects,默認值爲true,表示在YGC時收集大對象;有應用測試發現YGC時回收大對象會引發性能問題,若是遇到能夠關閉選項。
  • 參數G1EagerReclaimHumongousObjectsWithStaleRefs,默認值爲true,表示在YGC時斷定哪些大對象分區能夠收集,若是爲true表示當時大對象分區RSet的引用關係數小於G1RSetSparseRegionEntries(默認值爲0)能夠嘗試收集,若是爲false則只有RSet中的引用數爲0纔會收集。
相關文章
相關標籤/搜索