弄明白CMS和G1,就靠這一篇了

在開始介紹CMS和G1前,咱們能夠劇透幾點:java

  • 根據不一樣分代的特色,收集器可能不一樣。有些收集器能夠同時用於新生代和老年代,而有些時候,則須要分別爲新生代或老年代選用合適的收集器。通常來講,新生代收集器的收集頻率較高,應選用性能高效的收集器;而老年代收集器收集次數相對較少,對空間較爲敏感,應當避免選擇基於複製算法的收集器。
  • 在垃圾收集執行的時刻,應用程序須要暫停運行
  • 能夠串行收集,也能夠並行收集。
  • 若是能作到併發收集(應用程序沒必要暫停),那絕對是很妙的事情。
  • 若是收集行爲可控,那也是很妙的事情。

CMS和G1做爲垃圾收集器裏的大殺器,是須要好好弄明白的,並且面試中也常常被問到。面試

但願你們帶着下面的問題進行閱讀,有目標的閱讀,收穫更多:算法

  1. 爲何沒有一種牛逼的收集器像銀彈同樣適配全部場景?
  2. CMS的優勢、缺點、適用場景?
  3. 爲何CMS只能用做老年代收集器,而不能應用在新生代的收集?
  4. G1的優勢、缺點、適用場景?

1 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。這是由於CMS收集器工做時,GC工做線程與用戶線程能夠併發執行,以此來達到下降收集停頓時間的目的。安全

CMS收集器僅做用於老年代的收集,是基於標記-清除算法的,它的運做過程分爲4個步驟:併發

  • 初始標記(CMS initial mark)
  • 併發標記(CMS concurrent mark)
  • 從新標記(CMS remark)
  • 併發清除(CMS concurrent sweep)

其中,初始標記從新標記這兩個步驟仍然須要Stop-the-world。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記階段就是進行GC Roots Tracing的過程,而從新標記階段則是爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始階段稍長一些,但遠比並發標記的時間短。oracle

CMS以流水線方式拆分了收集週期,將耗時長的操做單元保持與應用線程併發執行。只將那些必需STW才能執行的操做單元單獨拎出來,控制這些單元在恰當的時機運行,並能保證僅需短暫的時間就能夠完成。這樣,在整個收集週期內,只有兩次短暫的暫停(初始標記和從新標記)達到了近似併發的目的app

CMS收集器優勢:併發收集、低停頓。jsp

CMS收集器缺點ide

  • CMS收集器對CPU資源很是敏感。
  • CMS收集器沒法處理浮動垃圾(Floating Garbage)。
  • CMS收集器是基於標記-清除算法,該算法的缺點都有。

CMS收集器之因此可以作到併發,根本緣由在於採用基於「標記-清除」的算法並對算法過程進行了細粒度的分解。前面篇章介紹過標記-清除算法將產生大量的內存碎片這對新生代來講是難以接受的,所以新生代的收集器並未提供CMS版本。

另外要補充一點,JVM在暫停的時候,須要選準一個時機。因爲JVM系統運行期間的複雜性,不可能作到隨時暫停,所以引入了安全點的概念。

安全點(Safepoint)

安全點,即程序執行時並不是在全部地方都能停頓下來開始GC,只有在到達安全點時才能暫停。Safepoint的選定既不能太少以致於讓GC等待時間太長,也不能過於頻繁以至於過度增大運行時的負荷。

安全點的初始目的並非讓其餘線程停下,而是找到一個穩定的執行狀態。在這個執行狀態下,Java虛擬機的堆棧不會發生變化。這麼一來,垃圾回收器便可以「安全」地執行可達性分析。只要不離開這個安全點,Java虛擬機便可以在垃圾回收的同時,繼續運行這段本地代碼。

程序運行時並不是在全部地方都能停頓下來開始GC,只有在到達安全點時才能暫停。安全點的選定基本上是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定的。「長時間執行」的最明顯特徵就是指令序列複用,例如方法調用、循環跳轉、異常跳轉等,因此具備這些功能的指令纔會產生Safepoint。

對於安全點,另外一個須要考慮的問題就是如何在GC發生時讓全部線程(這裏不包括執行JNI調用的線程)都「跑」到最近的安全點上再停頓下來。

兩種解決方案:

  • 搶先式中斷(Preemptive Suspension)

    搶先式中斷不須要線程的執行代碼主動去配合,在GC發生時,首先把全部線程所有中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它「跑」到安全點上。如今幾乎沒有虛擬機採用這種方式來暫停線程從而響應GC事件。

  • 主動式中斷(Voluntary Suspension)

    主動式中斷的思想是當GC須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立對象須要分配內存的地方。

安全區域

指在一段代碼片斷中,引用關係不會發生變化。在這個區域中任意地方開始GC都是安全的。也能夠把Safe Region看做是被擴展了的Safepoint。

2 G1收集器

G1從新定義了堆空間,打破了原有的分代模型,將堆劃分爲一個個區域。這麼作的目的是在進行收集時沒必要在全堆範圍內進行,這是它最顯著的特色。區域劃分的好處就是帶來了停頓時間可預測的收集模型:用戶能夠指定收集操做在多長時間內完成。即G1提供了接近實時的收集特性。

G1與CMS的特徵對好比下:

特徵 G1 CMS
併發和分代
最大化釋放堆內存
低延時
吞吐量
壓實
可預測性
新生代和老年代的物理隔離

G1具有以下特色:

  • 並行與併發:G1能充分利用多CPU、多核環境下的硬件優點,使用多個CPU來縮短Stop-the-world停頓的時間,部分其餘收集器原來須要停頓Java線程執行的GC操做,G1收集器仍然能夠經過併發的方式讓Java程序繼續運行。
  • 分代收集
  • 空間整合:與CMS的標記-清除算法不一樣,G1從總體來看是基於標記-整理算法實現的收集器,從局部(兩個Region之間)上來看是基於「複製」算法實現的。但不管如何,這兩種算法都意味着G1運做期間不會產生內存空間碎片,收集後能提供規整的可用內存。這種特性有利於程序長時間運行,分配大對象時不會由於沒法找到連續內存空間而提早觸發下一次GC
  • 可預測的停頓:這是G1相對於CMS的一個優點,下降停頓時間是G1和CMS共同的關注點。

在G1以前的其餘收集器進行收集的範圍都是整個新生代或者老年代,而G1再也不是這樣。在堆的結構設計時,G1打破了以往將收集範圍固定在新生代或老年代的模式,G1將堆分紅許多相同大小的區域單元,每一個單元稱爲Region。Region是一塊地址連續的內存空間,G1模塊的組成以下圖所示:

G1堆的Region佈局.png

G1收集器將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分Region(不須要連續)的集合。Region的大小是一致的,數值是在1M到32M字節之間的一個2的冪值數,JVM會盡可能劃分2048個左右、同等大小的Region,這一點能夠參看以下源碼。其實這個數字既能夠手動調整,G1也會根據堆大小自動進行調整。

#ifndef SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP
#define SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP

#include "memory/allocation.hpp"

class HeapRegionBounds : public AllStatic {
private:
  // Minimum region size; we won't go lower than that.
  // We might want to decrease this in the future, to deal with small
  // heaps a bit more efficiently.
  static const size_t MIN_REGION_SIZE = 1024 * 1024;

  // Maximum region size; we don't go higher than that. There's a good
  // reason for having an upper bound. We don't want regions to get too
  // large, otherwise cleanup's effectiveness would decrease as there
  // will be fewer opportunities to find totally empty regions after
  // marking.
  static const size_t MAX_REGION_SIZE = 32 * 1024 * 1024;

  // The automatic region size calculation will try to have around this
  // many regions in the heap (based on the min heap size).
  static const size_t TARGET_REGION_NUMBER = 2048;

public:
  static inline size_t min_size();
  static inline size_t max_size();
  static inline size_t target_number();
};

#endif // SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP

G1收集器之因此能創建可預測的停頓時間模型,是由於它能夠有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1會經過一個合理的計算模型,計算出每一個Region的收集成本並量化,這樣一來,收集器在給定了「停頓」時間限制的狀況下,老是能選擇一組恰當的Regions做爲收集目標,讓其收集開銷知足這個限制條件,以此達到實時收集的目的。

對於打算從CMS或者ParallelOld收集器遷移過來的應用,按照官方 的建議,若是發現符合以下特徵,能夠考慮更換成G1收集器以追求更佳性能:

  • 實時數據佔用了超過半數的堆空間;
  • 對象分配率或「晉升」的速度變化明顯;
  • 指望消除耗時較長的GC或停頓(超過0.5——1秒)。

原文以下:
Applications running today with either the CMS or the ParallelOld garbage collector would benefit switching to G1 if the application has one or more of the following traits.

  • More than 50% of the Java heap is occupied with live data.
  • The rate of object allocation rate or promotion varies significantly.
  • Undesired long garbage collection or compaction pauses (longer than 0.5 to 1 second)

G1收集的運做過程大體以下:

  • 初始標記(Initial Marking):僅僅只是標記一下GC Roots能直接關聯到的對象,而且修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可用的Region中建立新對象,這階段須要停頓線程,但耗時很短
  • 併發標記(Concurrent Marking):是從GC Roots開始堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序併發執行。
  • 最終標記(Final Marking):是爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs裏面,最終標記階段須要把Remembered Set Logs的數據合併到Remembered Set中,這階段須要停頓線程,可是可並行執行
  • 篩選回收(Live Data Counting and Evacuation):首先對各個Region的回收價值和成本進行排序,根據用戶所指望的GC停頓時間來制定回收計劃。這個階段也能夠作到與用戶程序一塊兒併發執行,可是由於只回收一部分Region,時間是用戶可控制的,並且停頓用戶線程將大幅提升收集效率。

全局變量和棧中引用的對象是能夠列入根集合的,這樣在尋找垃圾時,就能夠從根集合出發掃描堆空間。在G1中,引入了一種新的能加入根集合的類型,就是記憶集(Remembered Set)。Remembered Sets(也叫RSets)用來跟蹤對象引用。G1的不少開源都是源自Remembered Set,例如,它一般約佔Heap大小的20%或更高。而且,咱們進行對象複製的時候,由於須要掃描和更改Card Table的信息,這個速度影響了複製的速度,進而影響暫停時間。

image.png

卡表(Card Table)

有個場景,老年代的對象可能引用新生代的對象,那標記存活對象的時候,須要掃描老年代中的全部對象。由於該對象擁有對新生代對象的引用,那麼這個引用也會被稱爲GC Roots。那不是得又作全堆掃描?成本過高了吧。

HotSpot給出的解決方案是一項叫作卡表(Card Table)的技術。該技術將整個堆劃分爲一個個大小爲512字節的卡,而且維護一個卡表,用來存儲每張卡的一個標識位。這個標識位表明對應的卡是否可能存有指向新生代對象的引用。若是可能存在,那麼咱們就認爲這張卡是髒的。

在進行Minor GC的時候,咱們即可以不用掃描整個老年代,而是在卡表中尋找髒卡,並將髒卡中的對象加入到Minor GC的GC Roots裏。當完成全部髒卡的掃描以後,Java虛擬機便會將全部髒卡的標識位清零。

想要保證每一個可能有指向新生代對象引用的卡都被標記爲髒卡,那麼Java虛擬機須要截獲每一個引用型實例變量的寫操做,並做出對應的寫標識位操做。

卡表能用於減小老年代的全堆空間掃描,這能很大的提高GC效率

咱們能夠看下官方文檔對G1的展望(這段英文描述比較簡單,我就不翻譯了):

Future:
G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS). Comparing G1 with CMS, there are differences that make G1 a better solution. One difference is that G1 is a compacting collector. G1 compacts sufficiently to completely avoid the use of fine-grained free lists for allocation, and instead relies on regions. This considerably simplifies parts of the collector, and mostly eliminates potential fragmentation issues. Also, G1 offers more predictable garbage collection pauses than the CMS collector, and allows users to specify desired pause targets.

3 總結

查了下度娘有關G1的文章,絕大部分文章對G1的介紹都是停留在JDK7或更早期的實現不少結論已經存在較大誤差了,甚至一些過去的GC選項已經再也不推薦使用。舉個例子,JDK9中JVM和GC日誌進行了重構,如PrintGCDetails已經被標記爲廢棄,而PrintGCDateStamps已經被移除,指定它會致使JVM沒法啓動。

本文對CMS和G1的介紹絕大部份內容也是基於JDK7,新版本中的內容有一點介紹,倒沒作過多介紹(本人對新版本JVM尚未深刻研究),後面有機會能夠再出專門的文章來重點介紹。

4 參考

《深刻理解Java虛擬機》《HotSpot實戰》《極客時間專欄》

相關文章
相關標籤/搜索