堆內存常見的分配策略、 經典的垃圾收集器、CMS與G1收集器及二者的比較


堆內存常見的分配策略

針對的是Serial 加 Serial Old 客戶端默認收集器組合下的內存分配和回收策略

 經典的垃圾收集器

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的垃圾收集器。從名字可以看出,CMS 是基於標記-清除算法的。它的運作過程主要分為四個步驟:

  1. 初始標記(CMS initial mark):STW,標記GC Roots能直接關聯到的對象,速度很快。單線程
  2. 並發標記(CMS concurrent mark):從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,耗時較長,不需要停頓用戶線程
  3. 重新標記(CMS remark):STW,修正並發標記期間,因用戶程序繼續運作而導致標記發生變動的那一部分對象的標記記錄(增量更新),時間稍長於初始標記,但遠低於並發標記
  4. 並發清除(CMS concurrent sweep):清除已死亡對象,因為不需要移動對象,所以與用戶線程是並發的關系 

CMS 的缺點:

  • CMS收集器對處理器資源敏感。在並發階段,雖然不會導致用戶停頓(STW),但會占用一部分線程(計算機資源)導致應用程序變慢,吞吐量降低。CMS默認開啟回收線程數是 (處理器核心數量 + 3)/4。當處理器核心數量大於4個時,垃圾回收線程只占不超過 25%的處理器資源,且隨着處理器核心數量增加而降低。但處理器核心數量不足4個時,會嚴重影響用戶程序。為了緩解這個問題,虛擬機提供了增量式並發收集器的CMS變種,在並發標記和並發清理過程中,垃圾收集線程和用戶線程交替運行,但效果一般,已標記為廢棄。
  • 無法處理“浮動垃圾”,有可能出現 Concurrent Mode Failure失敗,導致進一步 STW 的Full GC在並發標記和並發清理過程中,用戶線程還在運行,自然還會伴隨新的垃圾對象的產生,但該部分的垃圾對象出現在標記過程結束以后,CMS無法在當次收集過程中處理他們(只能下次垃圾收集時處理),因此稱為浮動垃圾。因此,CMS必須預留足夠的內存空間提供給用戶線程使用,JDK6,默認啟動閾值為 92%(該值需要根據生產環境變化,太高,有可能頻繁並發失敗):要是CMS運行期間預留的內存無法滿足程序分配新對象,觸發並發失敗(Concurrent Mode Failure),這時候JVM啟動預備方案:凍結用戶線程,臨時啟動 Serial Old 來進行老年代的手機,這樣停頓時間會很長
  • 大量空間碎片。空間碎片過多會導致大對象分配失敗(老年代還有很多空間,但沒有連續的大空間),而不得不提前觸發 Full GC。CMS提供了參數 -XX:+UseCMS-CompactAtFullCollection 開關參數(默認開啟,JDK9之后廢棄),用於在CMS不得不Full GC時開啟碎片的合並整理過程。但整理涉及到移動對象,STW,停頓時間會變長。

 Garbage First (G1收集器)

目標是在延遲可控的范圍內獲取盡可能高的吞吐量

G1是面向堆內任何部分來組成回收集進行回收,衡量標准不再是它屬於哪個分代,而是哪塊內存中存放的垃圾最多,回收收益最大,這就是 G1 收集器的 Mixed GC 模式。 

G1不再堅持分代划分,而是把連續的Java 堆划分為多個大小相等的獨立區域(Region),每個 Region 根據需要版本新生代的Eden、Survivor或老年代。收集器能夠對扮演不同角色的Region采用不同的策略去處理。這樣無論是新對象還是已經存活了一段時間的對象、熬過多次收集的就對象都能獲得很好的收集效果。

Region 中還有一類特殊的 Humonggous 區域,專門用來存放大對象此處大對象,指的是內存大小超過一個 Region 容量一半的對象。每個Region的大小可以通過 -XX:G1HeapRegionSize 設定,取值范圍為 1~32MB,且為2的N次冪。如果對象大小超過Region大小,將會被放到N個連續的 Humongous Region,G1的大多數行為將Humongous 作為老年代的一部分處理。

雖然 G1 仍然保留了新生代和老年代的概念,但新生代和老年代不再是固定的,他們都是一系列區域(不需要連續)的動態集合。G1 之所以能夠建立可預測的停頓時間模型,是因為他將Region 作為可回收的最小單元。更具體的操作是:讓G1 去跟蹤各個Region 里面的垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及回收所需要的經驗值,然后在后台維護一個優先級列表,每次根據用戶設定的允許的收集停頓時間(-XX:MaxGCPauseMills,默認200ms)優先處理回收價值最大的Region。這也是 “Garbage First“ 的由來。該方式保證了G1在有限的事件內獲取盡可能高的收集效率。

 G1收集器大致可以分為以下四個步驟:

  1. 初始標記(initial marking):標記GC Roots 直接關聯的對象,並修改 TAMS 指針的值,讓下一階段用戶線程並發運行時,能正確的在可用的 Region中分配新對象。STW,耗時很短
  2. 並發標記(concurrent marking):從 GC Roots 開始對堆中對象進行可達性分析,遞歸掃面整個堆里的對象圖。耗時長,但可與用戶線程並發執行。對象圖掃描完成后,還要重新處理 SATB記錄下的在並發時有引用變動的對象
  3. 最終標記(final marking):暫停用戶線程(很短),用於處理並發階段結束后,遺留下來的最后少量的SATB記錄
  4. 篩選回收(Live data counting and evacuation 疏散、撤退):負責更新 Region 的統計數據,對各個Region的回收價值和成本進行排序,根據用戶期望的停頓時間來指定計划,可自由選擇任意多個 Region 進行回收。把決定回收的那部分的Region的存活對象復制到空的Region中,然后清理掉整個舊Region的空間。因為涉及對象的移動,所以需要STW,暫停用戶線程,由多條收集線程並行完成。

只有並發標記可以與用戶線程並發進行。

CMS 與 G1 比較

G1 的優點

  1. 收集算法不同,CMS 是標記—清除,G1從整體來看是基於”標記—整理“的,但從局部看(兩個Region之間),又是基於”復制“的。這意味着,G1不會產生內存碎片,垃圾回收完成后內存規整,在程序為大對象分配內存時不容易因為無法找到連續內存空間而提前觸發Full GC 
  2. 可以指定最大停頓時間
  3. 分 Region 的內存布局
  4. 按受益動態確定回收集

G1 的缺點:

  1. 內存占用而言,雖然 G1 和 CMS 都使用卡表來處理跨代指針,但G1 的實現更復雜,而且每個Region都需要維護一份卡表。而CMS 的卡表只有唯一一份,只需要處理老年代到新生代的引用。這導致G1的記憶集(和其他內存消耗)會占整個堆容量的 20%甚至更多。
  2. 執行負載而言,兩個收集器各自的實現細節導致了用戶程序運行時的負載會有不同。譬如,兩者都用到寫屏障,CMS 使用寫后屏障來維護卡表;而G1不僅使用了寫后屏障來維護卡表,為了實現原始快照搜索算法(SATB)還需要使用寫前屏障來跟蹤並發時的指針變化情況。相比增量更新算法,原始快照搜索算法能夠減少並發標記和重新標記階段的消耗,避免CMS 在最終標記階段停留時間過長的缺點,但在用戶程序運行期間確實會產生由跟蹤引用變化帶來的額外負擔。由於G1對寫屏障的復雜操作要比CMS消耗更多的運算資源,所以CMS的寫屏障實現是直接的同步操作,而G1采用的是類似消息隊列的結構,把寫前屏障和寫后屏障中要做的事放到隊列里,然后異步處理

CMS 更適合在小內存應用上,而G1在大內存應用上能發揮更多優勢。兩者的內存臨界點為 6 GB 到 8GB。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM