1、背景
G1(Garbage First Collector 垃圾優(yōu)先的收集器),說是一種全新的,其實G1垃圾收集器已經出現(xiàn)了N多年了,只是從發(fā)展到成熟是需要經歷一定的過程,oracle官方計劃在jdk9中將G1變成默認的垃圾收集器,以替代CMS, 可見G1肯定有它獨特的地方,它跟我們之前所學的各種垃圾底層是完全不一樣的,比如最明顯的不同是以前的分代收集方式是將堆劃分為新生代、老年代兩個區(qū)域,而新生代又劃分為Eden和兩個Survivor,也就是從物理的結構就明確的做了區(qū)域劃分,但是?。。1它依據的物理形態(tài)跟我們之前所接觸的垃圾收集器完全不一樣了,也就是明顯會感覺到G1里面的堆內存沒有明顯的區(qū)域劃分
2、吞吐量:
吞吐量關注的是,在一個指定的時間內,最大化一個應用的工作量。
如下方式來衡量一個系統(tǒng)吞吐量的好壞:
1、在一個小時內同一個事務(或者任務、請求)完成的次數(tps,實際中還會經常見qps,每秒查詢率QPS是對一個特定的查詢服務器在規(guī)定時間內所處理流量多少的衡量標準)。
2、數據庫一小時可以完成多少次查詢。對于關注吞吐量的系統(tǒng),卡頓是可以接受的,因為這個系統(tǒng)關注長時間的大量任務的執(zhí)行能力,單次快速的響應并不值得考慮。
3、響應能力:
- 響應能力指一個程序或者系統(tǒng)對請求是否能夠及時響應,比如:
1、一個桌面UI能多快地響應一個事件。
2、一個網站能夠多快返回一個頁面請求。
3、數據庫能夠多快返回查詢的數據。 - 對于這類對響應能力敏感的場景,長時間的停頓是無法接受的。
以上是用來評價一個系統(tǒng)的兩個很重要的指標,介紹這兩個指標的原因是因為G1就是用來解決這樣的問題而應運而生的。
4、G1垃圾回收器
- g1收集器是一個面向服務端的垃圾收集器,適用于多核處理器、大內存容量的服務端系統(tǒng)。
- 它滿足短時間gc停頓的同時達到一個較高的吞吐量。
- JDK7以上版本適用【通過配置JVM的參數來指定既可】。
以上可以看到G1在吞吐量和響應能力上都進行了兼顧。
5、G1收集器的設計目標:
(延遲可控的情況下獲得盡可能高的吞吐量)
- 與應用線程同時工作,幾乎【注意措辭】不需要stop the world(與CMS類似);
- 整理剩余空間,不產生內存碎片(CMS只能在Full GC時,用stop the world整理內存碎片)。
- GC停頓更加可控;【要是CMS運行期間預留的內存無法滿足程序需要時,虛擬機將啟動后備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。同時對于CMS來說如果出現(xiàn)了Full GC時,則會對新生代和老年代的堆內存進行完整的整理,停頓時間就不可控了】
- 不犧牲系統(tǒng)的吞吐量;
- gc不要求額外的內存空間(CMS需要預留空間存儲浮動垃圾【這個在學習CMS中已經闡述過了,其實就是CMS回收的過程跟用戶線程是并發(fā)進行的,所在在標記或者清除的同時對象的引用還會被改變,使得原來對象本來不是垃圾,當CMS清理時該對象已經變成了垃圾了,但是CMS認為它還不是垃圾,所以該對象的清除工作就會放到下一次了,所以將這種對象則稱之為浮動垃圾】)
6、G1內存模型
1、G1堆結構


(1)分區(qū) Region
G1 采用了分區(qū)(Region)的思路,堆被劃分為一個個相等的不連續(xù)的內存區(qū)域(regions),每個regions都有一個分代的角色:eden、survivor、old。對每個角色的數量并沒有強制的限定,也就是說對每種分代內存的大小,可以動態(tài)變化。每個分區(qū)也不會確定地為某個代服務,可以按需在年輕代和老年代之間切換。啟動時可以通過參數 -XX:G1HeapRegionSize=n 可指定分區(qū)大小(1MB~32MB,且必須是2的冪),默認將整堆劃分為2048個分區(qū)。
(2)卡片 Card
在每個分區(qū)內部又被分成了若干個大小為512 Byte 卡片(Card),標識堆內存最小可用粒度所有分區(qū)的卡片將會記錄在全局卡片表(Global Card Table)中,分配的對象會占用物理上連續(xù)的若干個卡片,當查找對分區(qū)內對象的引用時便可通過記錄卡片來查找該引用對象(見 RSet)。每次對內存的回收,都是對指定分區(qū)的卡片進行處理。默認情況下,每個卡都未被引用。當一個地址空間被引用時,這個地址空間對應的數組索引的值被標記為“0”,既標記為被引用,此外RSet也將這個數組下標記錄下來。一般情況下,這個RSet其實是一個Hash Table,key是當前Region的起始地址,Value是一個集合,里面的元素是Card Table的Index。
(3)已記憶集合 Remember Set(RSet)
在串行和并行收集器中,GC 通過整堆掃描,來確定對象是否處于可達路徑中。然而 G1 為了避免 STW 式的整堆掃描,在每個分區(qū)記錄了一個已記憶集合(RSet),內部類似一個反向指針,記錄引用分區(qū)內對象的卡片索引。當要回收該分區(qū)時,通過掃描分區(qū)的 RSet,來確定引用本分區(qū)內的對象是否存活,進而確定本分區(qū)內的對象存活情況。
事實上,并非所有的引用都需要記錄在 RSet 中,引用源自本分區(qū)的對象,當然不用落入 RSet 中;同時,G1 GC 每次都會對年輕代進行整體收集,因此引用源自年輕代的對象,也不需要在 RSet 中記錄。最后只有老年代的分區(qū)可能會有 RSet 記錄,這些分區(qū)稱為擁有 RSet 分區(qū)(an RSet’s owning region)。
(4)Humongous區(qū)域
在G1中,還有一種特殊的區(qū)域,叫Humongous區(qū)域。如果一個對象占用的空間達到或者超過了分區(qū)容量的50%以上,G1收集器就認為這是一個巨型對象。這些巨型對象,默認直接會被分配在老年代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響,為了解決這個問題,G1劃分 Humongous區(qū)域,它專門用來存放巨型對象。如果一個H區(qū)域裝不下一個巨型對象,那么G1會尋找連續(xù)的H分區(qū)來存儲。為了能找到連續(xù)的H區(qū),有時候不得不啟動Full GC
2、收集集合(CSet)

收集集合(CSet)代表每次 GC 暫停時回收的一系列目標分區(qū)。在任意一次收集暫停中,CSet 所有分區(qū)都會被釋放,內部存活的對象都會被轉移到分配的空閑分區(qū)中。因此無論是年輕代收集,還是混合收集,工作的機制都是一致的。
①:年輕代收集 CSet 只容納年輕代分區(qū),
②:混合收集會通過某種算法,在老年代候選回收分區(qū)中,篩選出回收收益最高的分區(qū)添加到 CSet 中。
該算法是:G1跟蹤各個Region里面的垃圾堆積的價值大?。ɑ厥账@得的空間大小以及回收所需要時間的經驗值),在后臺維護一個優(yōu)先列表,每次根據允許的收集時間,優(yōu)先回收價值最大的Region
年輕代收集集合 CSet of Young Collection(詳細版)
應用線程不斷活動后,年輕代空間會被逐漸填滿。當 JVM 分配對象到 Eden 區(qū)域失?。‥den 區(qū)已滿)時,便會觸發(fā)一次 STW 式的年輕代收集。在年輕代收集中,Eden 分區(qū)存活的對象將被拷貝到 Survivor 分區(qū);原有 Survivor 分區(qū)存活的對象,將根據任期閾值(tenuring threshold)分別晉升到 PLAB 中,新的 survivor 分區(qū)和老年代分區(qū)。而原有的年輕代分區(qū)將被整體回收掉。
同時,年輕代收集還負責維護對象的年齡(存活次數),輔助判斷老化(tenuring)對象晉升的時候是到 Survivor 分區(qū)還是到老年代分區(qū)。年輕代收集首先先將晉升對象尺寸總和、對象年齡信息維護到年齡表中,再根據年齡表、Survivor 尺寸、Survivor 填充容量 -XX:TargetSurvivorRatio(默認50%)、最大任期閾值 -XX:MaxTenuringThreshold(默認15),計算出一個恰當的任期閾值,凡是超過任期閾值的對象都會被晉升到老年代。
混合收集集合 CSet of Mixed Collection
年輕代收集不斷活動后,老年代的空間也會被逐漸填充。當老年代占用空間超過整堆比 IHOP 閾值 -XX:InitiatingHeapOccupancyPercent(默認45%)時,G1 就會啟動一次混合垃圾收集周期。為了滿足暫停目標,G1 可能不能一口氣將所有的候選分區(qū)收集掉,因此 G1 可能會產生連續(xù)多次的混合收集與應用線程交替執(zhí)行,每次 STW 的混合收集與年輕代收集過程相類似。
3、可預測的停頓模型
G1使用了gc停頓可預測的模型,來滿足用戶設定的gc停頓時間,根據用戶設定的目標時間,G1會自動地選擇哪些region要清除,一次清除多少個region。
這是G1相對于CMS的另一個大優(yōu)勢,G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒
- 由于分區(qū)的原因,G1可以只選部分區(qū)域進行內存回收,這樣縮小了回收的范圍,因此對于全局停頓情況的發(fā)生也能得到較好的控制
- G1跟蹤各個Region里面的垃圾堆積的價值大?。ɑ厥账@得的空間大小以及回收所需要時間的經驗值),在后臺維護一個優(yōu)先列表,每次根據允許的收集時間,優(yōu)先回收價值最大的Region。保證了G1收集器在有限的時間內可以獲取盡可能高的收集效率。
- 相比于CMS GC,G1未能做到CMS在最好情況下的延時停頓,但是最長情況要好很多
- 停頓時間的設置并不是越短越好,設置的時間越短意味著每次收集的Cset越小,導致垃圾逐步積累變多,最終不得不退化成Full GC(Serial GC),停頓時間設置過長,那么會導致每次都會產生長時間的停頓個,影響了程序對外的響應時間
7、G1垃圾回收器的缺點
1、相對于CMS,G1還不具備全方位、壓倒性的優(yōu)勢,比如在用戶程序運行過程中,G1無論是為了垃圾收集產生的內存占用還是程序運行時的額外執(zhí)行負載都要比CMS要高
2、從經驗上來說,在小內存應用上CMS的表現(xiàn)大概率會優(yōu)于G1,而G1在大內存應用則發(fā)揮其優(yōu)勢,平衡點6 ~ 8GB之間
8、G1垃圾回收過程
1、年輕代GC(Young GC)
年輕代垃圾回收只會回收Eden區(qū)和Survivor區(qū)。年輕代也使用了分區(qū)機制主要是因為便于代大小的調整
年輕代回收時,首先G1停止應用程序的執(zhí)行(Stop - The - World),G1創(chuàng)建回收集(CSet),對于YGC來說,整個年輕代(Eden區(qū) + Survivor區(qū))都是CSet
然后開始如下回收過程(并行操作,多個收集器的線程同時工作,但是用戶線程處于等待狀態(tài)。)
階段1:根掃描
GC Roots包括
(1). 虛擬機棧(棧幀中的局部變量區(qū),也叫做局部變量表)中引用的對象。
(2). 方法區(qū)中的類靜態(tài)屬性引用的對象。
(3). 方法區(qū)中常量引用的對象。
(4). 本地方法棧中JNI(Native方法)引用的對象。
階段2:更新RSet
處理dirty card隊列更新RS(更新完后,RSet可以準確的反映老年代對所在的內存分段中對象的引用)
對于應用程序的引用賦值語句Object.field = object,JVM會在之前和之后執(zhí)行特殊的操作以在dirty card queue中入隊一個保存了對象引用信息的card。在年輕代回收的時候,G1會對DIrty Card Queue中所有的card進行處理,以更新RSet,保證RSet實時準確的反映引用關系
那為什么不在引用賦值語句處直接更新RSet呢?這是為了性能的需要,RSet的處理需要線程同步,開銷會很大,使用隊列性能會好很多
階段3:處理RSet
識別被老年代對象指向的Eden中的對象,這些被指向的Eden中的對象被認為存活的對象
階段4:復制對象
拷貝存活的對象到survivor/old區(qū)域。① Survivor區(qū)內存中存活的對象如果年齡未達到閾值,年齡會加1,達到閾值會被賦值到Old區(qū)。 ② 如果Survivor空間不夠,Eden空間的部分數據會直接晉升到老年代空間
階段5:處理引用隊列
軟引用、弱引用、虛引用處理
2、并發(fā)標記過程(Concurrent Marking)
1、初始標記(inital mark,STW):它標記了從GC Root開始直接可達的對象,并且會觸發(fā)一次年輕代的GC
2、根區(qū)域掃描(Root Region Sacnning):觸發(fā)的年輕代GC完成后,所有新復制到 Survivor 分區(qū)(根區(qū)域)的對象,都需要被掃描并標記成根,G1 GC掃描Survivor取直接可達的老年代區(qū)域對象,并標記被引用的對象。這個過程必須在young GC之前完成
3、并發(fā)標記(Concurrent Marking):這個階段從GC Root開始對heap中的對象進行標記,標記線程與應用程序線程并發(fā)執(zhí)行,并且收集各個Region的存活對象信息。
4、重新標記(Remark,STW):由于應用程序持續(xù)進行,需要修正上一次的標記結果,SATB算法
5、獨占清理(Exclusive Cleanup,STW):計算各個區(qū)域的存活對象和GC回收比例,并進行排序,識別可以混合回收的區(qū)域。為混合回收做鋪墊,是STW的。這個階段不會實際上做垃圾的收集
6、并發(fā)清理(Concurrent Cleanup):清除空Region(沒有存活對象的),加入到free list。
3、混合回收(Mixed GC)
在并發(fā)標記過程后進行拷貝存活對象