在開始介紹CMS和G1前私恬,我們可以劇透幾點:
- 根據(jù)不同分代的特點债沮,收集器可能不同。有些收集器可以同時用于新生代和老年代本鸣,而有些時候疫衩,則需要分別為新生代或老年代選用合適的收集器。一般來說荣德,新生代收集器的收集頻率較高闷煤,應選用性能高效的收集器;而老年代收集器收集次數(shù)相對較少涮瞻,對空間較為敏感鲤拿,應當避免選擇基于復制算法的收集器。
- 在垃圾收集執(zhí)行的時刻署咽,應用程序需要暫停運行近顷。
- 可以串行收集,也可以并行收集宁否。
- 如果能做到并發(fā)收集(應用程序不必暫停)窒升,那絕對是很妙的事情。
- 如果收集行為可控家淤,那也是很妙的事情异剥。
CMS和G1作為垃圾收集器里的大殺器,是需要好好弄明白的絮重,而且面試中也經(jīng)常被問到冤寿。
希望大家?guī)е旅娴膯栴}進行閱讀,有目標的閱讀青伤,收獲更多:
- 為什么沒有一種牛逼的收集器像銀彈一樣適配所有場景督怜?
- CMS的優(yōu)點、缺點狠角、適用場景号杠?
- 為什么CMS只能用作老年代收集器,而不能應用在新生代的收集丰歌?
- G1的優(yōu)點姨蟋、缺點、適用場景立帖?
1 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器眼溶。這是因為CMS收集器工作時,GC工作線程與用戶線程可以并發(fā)
執(zhí)行晓勇,以此來達到降低收集停頓時間的目的堂飞。
CMS收集器僅作用于老年代的收集,是基于標記-清除算法
的绑咱,它的運作過程分為4個步驟:
- 初始標記(CMS initial mark)
- 并發(fā)標記(CMS concurrent mark)
- 重新標記(CMS remark)
- 并發(fā)清除(CMS concurrent sweep)
其中绰筛,初始標記
、重新標記
這兩個步驟仍然需要Stop-the-world描融。初始標記僅僅只是標記一下GC Roots能直接關(guān)聯(lián)到的對象铝噩,速度很快,并發(fā)標記階段就是進行GC Roots Tracing的過程窿克,而重新標記階段則是為了修正并發(fā)標記期間因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那一部分對象的標記記錄骏庸,這個階段的停頓時間一般會比初始階段稍長一些,但遠比并發(fā)標記的時間短让歼。
CMS以流水線方式拆分了收集周期敞恋,將耗時長的操作單元保持與應用線程并發(fā)執(zhí)行。只將那些必需STW才能執(zhí)行的操作單元單獨拎出來谋右,控制這些單元在恰當?shù)臅r機運行硬猫,并能保證僅需短暫的時間就可以完成。這樣改执,在整個收集周期內(nèi)啸蜜,只有兩次短暫的暫停(初始標記和重新標記),達到了近似并發(fā)的目的辈挂。
CMS收集器優(yōu)點:并發(fā)收集衬横、低停頓。
CMS收集器缺點:
- CMS收集器對CPU資源非常敏感终蒂。
- CMS收集器無法處理浮動垃圾(Floating Garbage)蜂林。
- CMS收集器是基于標記-清除算法遥诉,該算法的缺點都有。
CMS收集器之所以能夠做到并發(fā)噪叙,根本原因在于采用基于“標記-清除”的算法并對算法過程進行了細粒度的分解矮锈。前面篇章介紹過標記-清除算法將產(chǎn)生大量的內(nèi)存碎片這對新生代來說是難以接受的,因此新生代的收集器并未提供CMS版本睁蕾。
另外要補充一點苞笨,JVM在暫停的時候,需要選準一個時機子眶。由于JVM系統(tǒng)運行期間的復雜性瀑凝,不可能做到隨時暫停,因此引入了安全點的概念臭杰。
安全點(Safepoint)
安全點粤咪,即程序執(zhí)行時并非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停硅卢。Safepoint的選定既不能太少以至于讓GC等待時間太長射窒,也不能過于頻繁以致于過分增大運行時的負荷。
安全點的初始目的并不是讓其他線程停下将塑,而是找到一個穩(wěn)定的執(zhí)行狀態(tài)脉顿。在這個執(zhí)行狀態(tài)下,Java虛擬機的堆棧不會發(fā)生變化点寥。這么一來艾疟,垃圾回收器便能夠“安全”地執(zhí)行可達性分析。只要不離開這個安全點敢辩,Java虛擬機便能夠在垃圾回收的同時蔽莱,繼續(xù)運行這段本地代碼。
程序運行時并非在所有地方都能停頓下來開始GC戚长,只有在到達安全點時才能暫停盗冷。安全點的選定基本上是以程序“是否具有讓程序長時間執(zhí)行的特征”為標準進行選定的⊥“長時間執(zhí)行”的最明顯特征就是指令序列復用仪糖,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)迫肖、異常跳轉(zhuǎn)等锅劝,所以具有這些功能的指令才會產(chǎn)生Safepoint。
對于安全點蟆湖,另一個需要考慮的問題就是如何在GC發(fā)生時讓所有線程(這里不包括執(zhí)行JNI調(diào)用的線程)都“跑”到最近的安全點上再停頓下來故爵。
兩種解決方案:
-
搶先式中斷(Preemptive Suspension)
搶先式中斷不需要線程的執(zhí)行代碼主動去配合,在GC發(fā)生時隅津,首先把所有線程全部中斷诬垂,如果發(fā)現(xiàn)有線程中斷的地方不在安全點上劲室,就恢復線程,讓它“跑”到安全點上“祝現(xiàn)在幾乎沒有虛擬機采用這種方式來暫停線程從而響應GC事件痹籍。
-
主動式中斷(Voluntary Suspension)
主動式中斷的思想是當GC需要中斷線程的時候呢铆,不直接對線程操作晦鞋,僅僅簡單地設(shè)置一個標志,各個線程執(zhí)行時主動去輪詢這個標志棺克,發(fā)現(xiàn)中斷標志為真時就自己中斷掛起悠垛。輪詢標志的地方和安全點是重合的,另外再加上創(chuàng)建對象需要分配內(nèi)存的地方娜谊。
安全區(qū)域
指在一段代碼片段中确买,引用關(guān)系不會發(fā)生變化。在這個區(qū)域中任意地方開始GC都是安全的纱皆。也可以把Safe Region看作是被擴展了的Safepoint湾趾。
2 G1收集器
G1重新定義了堆空間,打破了原有的分代模型派草,將堆劃分為一個個區(qū)域搀缠。這么做的目的是在進行收集時不必在全堆范圍內(nèi)進行,這是它最顯著的特點近迁。區(qū)域劃分的好處就是帶來了停頓時間可預測的收集模型:用戶可以指定收集操作在多長時間內(nèi)完成艺普。即G1提供了接近實時的收集特性。
G1與CMS的特征對比如下:
特征 | G1 | CMS |
---|---|---|
并發(fā)和分代 | 是 | 是 |
最大化釋放堆內(nèi)存 | 是 | 否 |
低延時 | 是 | 是 |
吞吐量 | 高 | 低 |
壓實 | 是 | 否 |
可預測性 | 強 | 弱 |
新生代和老年代的物理隔離 | 否 | 是 |
G1具備如下特點:
- 并行與并發(fā):G1能充分利用多CPU鉴竭、多核環(huán)境下的硬件優(yōu)勢歧譬,使用多個CPU來縮短Stop-the-world停頓的時間,部分其他收集器原來需要停頓Java線程執(zhí)行的GC操作搏存,G1收集器仍然可以通過并發(fā)的方式讓Java程序繼續(xù)運行瑰步。
- 分代收集
- 空間整合:與CMS的標記-清除算法不同,G1從整體來看是基于標記-整理算法實現(xiàn)的收集器璧眠,從局部(兩個Region之間)上來看是基于“復制”算法實現(xiàn)的缩焦。但無論如何,這兩種算法都意味著G1運作期間不會產(chǎn)生內(nèi)存空間碎片蛆橡,收集后能提供規(guī)整的可用內(nèi)存舌界。這種特性有利于程序長時間運行遵班,分配大對象時不會因為無法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次GC硬爆。
- 可預測的停頓:這是G1相對于CMS的一個優(yōu)勢,降低停頓時間是G1和CMS共同的關(guān)注點媚值。
在G1之前的其他收集器進行收集的范圍都是整個新生代或者老年代睦焕,而G1不再是這樣藐握。在堆的結(jié)構(gòu)設(shè)計時靴拱,G1打破了以往將收集范圍固定在新生代或老年代的模式,G1將堆分成許多相同大小的區(qū)域單元猾普,每個單元稱為Region袜炕。Region是一塊地址連續(xù)的內(nèi)存空間,G1模塊的組成如下圖所示:
G1收集器將整個Java堆劃分為多個大小相等的獨立區(qū)域(Region)初家,雖然還保留有新生代和老年代的概念偎窘,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續(xù))的集合溜在。Region的大小是一致的陌知,數(shù)值是在1M到32M字節(jié)之間的一個2的冪值數(shù),JVM會盡量劃分2048個左右掖肋、同等大小的Region仆葡,這一點可以參看如下源碼。其實這個數(shù)字既可以手動調(diào)整志笼,G1也會根據(jù)堆大小自動進行調(diào)整沿盅。
#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堆中進行全區(qū)域的垃圾收集纫溃。G1會通過一個合理的計算模型腰涧,計算出每個Region的收集成本并量化,這樣一來皇耗,收集器在給定了“停頓”時間限制的情況下南窗,總是能選擇一組恰當?shù)腞egions作為收集目標,讓其收集開銷滿足這個限制條件郎楼,以此達到實時收集的目的万伤。
對于打算從CMS或者ParallelOld收集器遷移過來的應用,按照官方 的建議呜袁,如果發(fā)現(xiàn)符合如下特征敌买,可以考慮更換成G1收集器以追求更佳性能:
- 實時數(shù)據(jù)占用了超過半數(shù)的堆空間;
- 對象分配率或“晉升”的速度變化明顯阶界;
- 期望消除耗時較長的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能直接關(guān)聯(lián)到的對象,并且修改TAMS(Next Top at Mark Start)的值膘融,讓下一階段用戶程序并發(fā)運行時芙粱,能在正確可用的Region中創(chuàng)建新對象,這階段需要
停頓線程
氧映,但耗時很短春畔。 - 并發(fā)標記(Concurrent Marking):是從GC Roots開始堆中對象進行可達性分析,找出存活的對象,這階段耗時較長律姨,但可與用戶程序并發(fā)執(zhí)行振峻。
-
最終標記(Final Marking):是為了修正并發(fā)標記期間因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs里面择份,最終標記階段需要把Remembered Set Logs的數(shù)據(jù)合并到Remembered Set中扣孟,這階段需要
停頓線程
,但是可并行執(zhí)行荣赶。 - 篩選回收(Live Data Counting and Evacuation):首先對各個Region的回收價值和成本進行排序凤价,根據(jù)用戶所期望的GC停頓時間來制定回收計劃。這個階段也可以做到與用戶程序一起并發(fā)執(zhí)行讯壶,但是因為只回收一部分Region料仗,時間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率伏蚊。
全局變量和棧中引用的對象是可以列入根集合的,這樣在尋找垃圾時格粪,就可以從根集合出發(fā)掃描堆空間躏吊。在G1中,引入了一種新的能加入根集合的類型帐萎,就是記憶集
(Remembered Set)比伏。Remembered Sets(也叫RSets)用來跟蹤對象引用。G1的很多開源都是源自Remembered Set疆导,例如赁项,它通常約占Heap大小的20%或更高。并且澈段,我們進行對象復制的時候悠菜,因為需要掃描和更改Card Table的信息,這個速度影響了復制的速度败富,進而影響暫停時間悔醋。
卡表(Card Table)
有個場景,老年代的對象可能引用新生代的對象兽叮,那標記存活對象的時候芬骄,需要掃描老年代中的所有對象。因為該對象擁有對新生代對象的引用鹦聪,那么這個引用也會被稱為GC Roots账阻。那不是得又做全堆掃描?成本太高了吧泽本。
HotSpot給出的解決方案是一項叫做卡表
(Card Table)的技術(shù)淘太。該技術(shù)將整個堆劃分為一個個大小為512字節(jié)的卡,并且維護一個卡表,用來存儲每張卡的一個標識位琴儿。這個標識位代表對應的卡是否可能存有指向新生代對象的引用段化。如果可能存在,那么我們就認為這張卡是臟的造成。
在進行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 總結(jié)
查了下度娘有關(guān)G1的文章棘脐,絕大部分文章對G1的介紹都是停留在JDK7或更早期的實現(xiàn)很多結(jié)論已經(jīng)存在較大偏差了,甚至一些過去的GC選項已經(jīng)不再推薦使用龙致。舉個例子蛀缝,JDK9中JVM和GC日志進行了重構(gòu),如PrintGCDetails已經(jīng)被標記為廢棄目代,而PrintGCDateStamps已經(jīng)被移除屈梁,指定它會導致JVM無法啟動。
本文對CMS和G1的介紹絕大部分內(nèi)容也是基于JDK7榛了,新版本中的內(nèi)容有一點介紹在讶,倒沒做過多介紹(本人對新版本JVM還沒有深入研究),后面有機會可以再出專門的文章來重點介紹忽冻。
4 參考
《深入理解Java虛擬機》《HotSpot實戰(zhàn)》《極客時間專欄》