在開始介紹CMS和G1前,咱們能夠劇透幾點:html
CMS和G1做爲垃圾收集器裏的大殺器,是須要好好弄明白的,並且面試中也常常被問到。java
但願你們帶着下面的問題進行閱讀,有目標的閱讀,收穫更多:面試
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。這是由於CMS收集器工做時,GC工做線程與用戶線程能夠併發
執行,以此來達到下降收集停頓時間的目的。算法
CMS收集器僅做用於老年代的收集,是基於標記-清除算法
的,它的運做過程分爲4個步驟:安全
其中,初始標記
、從新標記
這兩個步驟仍然須要Stop-the-world。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記階段就是進行GC Roots Tracing的過程,而從新標記階段則是爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始階段稍長一些,但遠比並發標記的時間短。bash
CMS以流水線方式拆分了收集週期,將耗時長的操做單元保持與應用線程併發執行。只將那些必需STW才能執行的操做單元單獨拎出來,控制這些單元在恰當的時機運行,並能保證僅需短暫的時間就能夠完成。這樣,在整個收集週期內,只有兩次短暫的暫停(初始標記和從新標記),達到了近似併發的目的。併發
CMS收集器優勢:併發收集、低停頓。oracle
CMS收集器缺點:app
CMS收集器之因此可以作到併發,根本緣由在於採用基於「標記-清除」的算法並對算法過程進行了細粒度的分解。前面篇章介紹過標記-清除算法將產生大量的內存碎片這對新生代來講是難以接受的,所以新生代的收集器並未提供CMS版本。jsp
另外要補充一點,JVM在暫停的時候,須要選準一個時機。因爲JVM系統運行期間的複雜性,不可能作到隨時暫停,所以引入了安全點的概念。
安全點,即程序執行時並不是在全部地方都能停頓下來開始GC,只有在到達安全點時才能暫停。Safepoint的選定既不能太少以致於讓GC等待時間太長,也不能過於頻繁以至於過度增大運行時的負荷。
安全點的初始目的並非讓其餘線程停下,而是找到一個穩定的執行狀態。在這個執行狀態下,Java虛擬機的堆棧不會發生變化。這麼一來,垃圾回收器便可以「安全」地執行可達性分析。只要不離開這個安全點,Java虛擬機便可以在垃圾回收的同時,繼續運行這段本地代碼。
程序運行時並不是在全部地方都能停頓下來開始GC,只有在到達安全點時才能暫停。安全點的選定基本上是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定的。「長時間執行」的最明顯特徵就是指令序列複用,例如方法調用、循環跳轉、異常跳轉等,因此具備這些功能的指令纔會產生Safepoint。
對於安全點,另外一個須要考慮的問題就是如何在GC發生時讓全部線程(這裏不包括執行JNI調用的線程)都「跑」到最近的安全點上再停頓下來。
兩種解決方案:
搶先式中斷(Preemptive Suspension)
搶先式中斷不須要線程的執行代碼主動去配合,在GC發生時,首先把全部線程所有中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它「跑」到安全點上。如今幾乎沒有虛擬機採用這種方式來暫停線程從而響應GC事件。
主動式中斷(Voluntary Suspension)
主動式中斷的思想是當GC須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立對象須要分配內存的地方。
指在一段代碼片斷中,引用關係不會發生變化。在這個區域中任意地方開始GC都是安全的。也能夠把Safe Region看做是被擴展了的Safepoint。
G1從新定義了堆空間,打破了原有的分代模型,將堆劃分爲一個個區域。這麼作的目的是在進行收集時沒必要在全堆範圍內進行,這是它最顯著的特色。區域劃分的好處就是帶來了停頓時間可預測的收集模型:用戶能夠指定收集操做在多長時間內完成。即G1提供了接近實時的收集特性。
G1與CMS的特徵對好比下:
特徵 | G1 | CMS |
---|---|---|
併發和分代 | 是 | 是 |
最大化釋放堆內存 | 是 | 否 |
低延時 | 是 | 是 |
吞吐量 | 高 | 低 |
壓實 | 是 | 否 |
可預測性 | 強 | 弱 |
新生代和老年代的物理隔離 | 否 | 是 |
G1具有以下特色:
在G1以前的其餘收集器進行收集的範圍都是整個新生代或者老年代,而G1再也不是這樣。在堆的結構設計時,G1打破了以往將收集範圍固定在新生代或老年代的模式,G1將堆分紅許多相同大小的區域單元,每一個單元稱爲Region。Region是一塊地址連續的內存空間,G1模塊的組成以下圖所示:
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收集器以追求更佳性能:
原文以下: 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收集的運做過程大體以下:
停頓線程
,但耗時很短。停頓線程
,可是可並行執行。全局變量和棧中引用的對象是能夠列入根集合的,這樣在尋找垃圾時,就能夠從根集合出發掃描堆空間。在G1中,引入了一種新的能加入根集合的類型,就是記憶集
(Remembered Set)。Remembered Sets(也叫RSets)用來跟蹤對象引用。G1的不少開源都是源自Remembered Set,例如,它一般約佔Heap大小的20%或更高。而且,咱們進行對象複製的時候,由於須要掃描和更改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.
查了下度娘有關G1的文章,絕大部分文章對G1的介紹都是停留在JDK7或更早期的實現不少結論已經存在較大誤差了,甚至一些過去的GC選項已經再也不推薦使用。舉個例子,JDK9中JVM和GC日誌進行了重構,如PrintGCDetails已經被標記爲廢棄,而PrintGCDateStamps已經被移除,指定它會致使JVM沒法啓動。
本文對CMS和G1的介紹絕大部份內容也是基於JDK7,新版本中的內容有一點介紹,倒沒作過多介紹(本人對新版本JVM尚未深刻研究),後面有機會能夠再出專門的文章來重點介紹。
《深刻理解Java虛擬機》 《HotSpot實戰》 《極客時間專欄》