上一節主要介紹了新生代的 Serial / PraNew / Parallel Scavenge 三種垃圾回收方法和老年代的serial old 和 paralle old收集器,本節主要介紹CMS和G1垃圾收集器。
目錄
CMS收集器-標記整理算法
CMS(Concurrent Mark Sweep)收集器的設計目標是:獲取最短回收停頓時間的收集器。
HotSpot在JDK1.5
推出的第一款真正意義上的併發(Concurrent)收集器; 第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。目前很大一部分的Java應用集中在B/S系統的服務端上,這類應用尤其重視服務器的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。而這也恰恰是CMS所擅長的地方。
收集過程
CMS的垃圾收集一共需要經過四個階段:
- 初始標記:此階段會產生STW;這個過程只是標記出GC ROOTS能直接關聯到的對象,耗時短,速度很快;
- 併發標記: 這個步是根據前面提到的GC ROOTS 根搜索算法,會判定對象是“存活”還是“已死”;比如說 A -> B (A 引用 B,假設 A 是 GC Roots 關聯到的對象),那麼這個階段就是標記出 B 對象, A 對象會在初始標記中標記出來。
- 重新標記: 此階段會產生STW,主要是爲了檢查校驗併發標記期間,因用戶程序繼續運行而產生變動的那一部分對象的標記記錄和新對象;
- 併發清除:該步就是清除系統中無用“以死”的對象;之後將爲下次gc做準備;
優點 | 併發收集,此階段比較耗時;由於採用併發收集可以減少停頓時間; |
缺點 |
(1)由於和用戶線程一起工作,CMS收集器對CPU資源非常敏感。CPU個數少於4個時,CMS對於用戶程序的影響就可能變得很大,出現cpu資源的競爭; (2)CMS收集器無法處理浮動垃圾,可能出現“promotion fail -> Concurrent Mode Failure”失敗而導致另一次Full GC的產生。在JDK1.5的默認設置下,CMS收集器當老年代使用了68%的空間後就會被激活; (3)CMS是基於“標記-清除”算法實現的收集器,手機結束時會有大量空間碎片產生。空間碎片過多,可能會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前對象,不得不提前出發FullGC; (4) 產生2次STW; |
使用場景 | 重視服務器的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。犧牲吞吐量來獲取較小的暫停時間。 |
參數使用 |
CMS 的 GC 日誌 就是 CMS。 |
增量式併發收集器
爲了解決缺點(1)cpu敏感的問題,虛擬機提供了一種稱爲“增量式併發收集器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器變種,所做的事情和單CPU年代PC機操作系統使用搶佔式來模擬多任務機制的思想一樣,就是在併發標記、清理的時候讓GC線程、用戶線程交替運行,儘量減少GC線程的獨佔資源的時間,這樣整個垃圾收集的過程會更長,但對用戶程序的影響就會顯得少一些,也就是速度下降沒有那麼明顯。實踐證明,增量時的CMS收集器效果很一般,在目前版本中,i-CMS已經被聲明爲“deprecated”,即不再提倡用戶使用。
CMS 產生的問題
CMS並行GC是大多數應用的最佳選擇,然而, CMS並不是完美的,在使用CMS的過程中會產生2個最讓人頭痛的問題:
- promotion failed
- concurrent mode failure
問題解析: 第一個問題promotion failed是在進行Minor GC時,Survivor Space放不下,對象只能放入老年代,而此時老年代也放不下造成的,多數是由於老年帶有足夠的空閒空間,但是由於碎片較多,這時如果新生代要轉移到老年帶的對象比較大,所以,必須儘可能提早觸發老年代的CMS回收來避免這個問題(promotion failed時老年代CMS還沒有機會進行回收,又放不下轉移到老年帶的對象,CMS運行期間預留的內存無法滿足程序其他線程需要,因此會出現下一個問題concurrent mode failure,從而回退到:stop-the-wold GC- Serail Old)。
儘管CMS使用一個叫做分配擔保的機制,每次Minor GC之後要保證新生代的空間survivor + eden > 老年帶的空閒空間,但是對象分配是不可預測的,總會有寫對象分配在老年帶是滿足不了的。
這個問題的直接影響就是它會導致提前進行CMS Full GC, 儘管這個時候CMS的老年帶並沒有填滿,只不過有過多的碎片而已,但是Full GC導致的stop-the-wold是難以接受的。
導致以上兩個問題的主要原因是:空間不夠,或者是因爲空間總量夠,但是由於碎片導致了沒有連續的大的存儲空間。
對於問題“promotion failed” 解決辦法:
- -XX:UseCMSCompactAtFullCollection 每次CMS完進行碎片的整理;-XX:CMSFullGCBeforeCompaction=5 5次CMS後進行一次
- -Xmn XX:SurvivorRatio 默認爲8,通過它調大新生代或者救助空間 ;
對於問題“concurrent mode failure” 解決辦法:
- +XX:CMSInitiatingOccupancyFraction 提前進行CMS垃圾回收,默認的值是68%,可以適當的調小;
- -Xms -Xmx調大老年帶的空間;
G1(Garbage First)收集器
這是一款兼顧新生代和老年代垃圾收集器,是JDK1.7提供的一個新的面向服務端應用的垃圾收集器,用於取代CMS垃圾回收。
收集過程
G1收集器的運行步驟主要分爲以下4步:
1. 初始標記(Initial Marking)
該階段會STW。掃描根集合,僅標記一下GC Roots能直接關聯到的對象,將所有通過根集合直達的對象壓入掃描棧,等待後續的處理。在G1中初始標記階段是藉助Young GC的暫停進行的,不需要額外的暫停。雖然加長了Young GC的暫停時間,但是從總體上來說還是提高的GC的效率。
2. 併發標記(Concurrent Marking)
該階段不需要STW。這個階段不斷的從掃描棧中取出對象進行掃描,將掃描到的對象的字段再壓入掃描棧中,依次遞歸,直到掃描棧爲空,也就是說trace了所有GCRoot直達的對象。同時這個階段還會掃描SATB write barrier所記錄下的引用。此步比較耗時,但是和應用程序一起工作,併發執行,和CMS一樣,效率提高。
3. 最終標記(Final Marking)
這個階段會STW,最終標記主要爲了找出程序在併發標記期間因用戶程序繼續運作而發生變化的對象。這個階段會處理在併發標記階段write barrier記錄下的引用,同時進行弱引用的處理。這個階段與CMS的最大的區別是CMS在這個階段會掃描整個根集合,Eden也會作爲根集合的一部分被掃描,因此耗時可能會很長。
4. 篩選回收(Live Data Counting and Evacuation)
該階段會STW。清點和重置標記狀態。這個階段有點像mark-sweep中的sweep階段,這個階段並不會實際上去做垃圾的收集,只是去根據停頓模型在CSet選出任意多個Region作爲垃圾收集的目標,等待evacuation階段來回收。篩選就是依據用戶設置【-XX:MaxGCPauseMillis 】的允許的GC時長,在Cset裏對排序的各個Region的回收價值和成本預估,控制GC停頓時間來制定回收計劃,達到用戶的期望;
G1特點
簡單調優
G1的設計原則就是簡單可行的性能調優,開發人員僅僅需要聲明以下參數即可:
-XX:+UseG1GC -Xmx4g -XX:MaxGCPauseMillis=200
- -XX:+UseG1GC //爲開啓G1垃圾收集器,
- -Xmx4g //設計堆內存的最大內存爲4G,
- -XX:MaxGCPauseMillis=200 //設置GC的最大暫停時間爲200ms。
如果我們需要調優,在內存大小一定的情況下,我們只需要修改最大暫停時間即可,簡單方便。G1將新生代,老年代的物理空間劃分取消了。
區域(Region)
G1裏面的Region的概念不同於傳統的垃圾回收算法中的分區的概念,但是仍然保留裏分代的思想。G1默認把堆內存分爲1024個分區,後續垃圾收集的單位都是以Region爲單位的,仍然屬於分代收集器。Region是實現G1算法的基礎,每個Region的大小相等,通過-XX:G1HeapRegionSize參數可以設置Region的大小,這樣我們再也不用單獨的空間對每個代進行設置了,不用擔心每個代內存是否足夠。
從圖8中可以看出各個區域邏輯上並不是連續的。並且一個Region在某一個時刻是Eden,在另一個時刻就可能屬於老年代。G1在進行垃圾清理的時候就是將一個Region的對象拷貝到另外一個Region中。
Humongous區域
在G1中,還有一種特殊的區域,叫Humongous區域。 如果一個對象佔用的空間超過了分區容量50%以上,G1收集器就認爲這是一個巨型對象。這些巨型對象,默認直接會被分配在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。爲了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那麼G1會尋找連續的H分區來存儲。爲了能找到連續的H區,有時候不得不啓動Full GC。
對象的分配策略
對象的分配策略分爲3個階段:
- TLAB(Thread Local Allocation Buffer)線程本地分配緩衝區;
- Eden區中分配;
- Humongous區分配;
TLAB爲線程本地分配緩衝區,它的目的爲了使對象儘可能快的分配出來。如果對象在一個共享的空間中分配,我們需要採用一些同步機制來管理這些空間內的空閒空間指針。在Eden空間中,每一個線程都有一個固定的分區用於分配對象,即一個TLAB。分配對象時,線程之間不再需要進行任何的同步。對TLAB空間中無法分配的對象 -> JVM會嘗試在Eden空間中進行分配 -> 如果Eden空間無法容納該對象,就只能在老年代中進行分配空間。
G1的垃圾收集模式
- YoungGC:收集年輕代裏的Region;
- MixGC:年輕代的所有Region+全局併發標記階段選出的收益高的Region;
Young GC
工作方式:Young GC主要是對Eden區進行GC,它在Eden空間耗盡時會被觸發。在這種情況下,Eden空間的數據移動到Survivor空間中,如果Survivor空間不夠,Eden空間的部分數據會直接晉升到年老代空間。Survivor區的數據移動到新的Survivor區中,也有部分數據晉升到老年代空間中。最終Eden空間的數據爲空,GC停止工作,應用線程繼續執行。
Mixed GC
如名字一樣,“混合收集” 不僅進行正常的新生代垃圾收集,同時也回收線程標記的老年代分區。它的GC步驟主要分2步:
(1)全局併發標記(global concurrent marking),在G1 gc之前,會進行全局標記;
(2)拷貝存活對象(evacuation),排序預估回收;
具體運行步驟可參考圖1。下面我們來看一下在G1工作的過程中的幾個重要的問題。
G1問題
1、如果僅僅是GC回收新生代對象,如何解決不同Region區域的引用,如何找到所有的根對象呢?
在垃圾回收的時候都是從Root開始搜索,這會先經過年輕代再到老年代,對於年輕代引用老年代的這種跨代不需要單獨處理。但是老年代引用年輕代的會影響young gc,這種跨代需要處理。這裏CMS和G1都用到了Card Table,一個Card Table將一個分區在邏輯上劃分爲固定大小的連續區域,每個區域稱之爲卡,卡通常較小,一個字節對應一個Card。當一個Card上的對象的引用發生變化的時候,就將這個Card對應的Card Table上的狀態置爲dirty,young gc的時候掃描狀態是dirty的Card即可,CMS的老年代就會記錄這樣一個Card tbale。對於G1垃圾收集,又引入了Rset(Remembered Set),在老年代中有一塊區域用來記錄指向新生代的引用。 無論G1還是其他分代收集器,JVM都是使用Card table來避免全局掃描。
我們知道G1垃圾收集器將內存分爲了不同的Region區域,不再以嚴格的年輕代和老年代來區分內存並進行垃圾回收,如果需要掃描整個old區,勢必會浪費很多的時間,且掃描了一些不必要的Region區域。 G1通過RSet,每個Region中都有一個RSet,記錄的是其他Region中的對象引用本Region對象的關係,是一種point-in的關係,即:誰引用了我的對象。因爲G1分了很多Region,需要回收那個區域的時候,只需要判斷要回收的區域是否有其他對象引用了該區域裏的對象,即只需要找待回收區域的根對象即可,避免無效掃描。若存儲point-out關係,將會掃描很多無關的Region區,造成時間性能的浪費。這裏面還有另外一個集合:Collection Set,簡稱:CSet,CSet記錄的是GC要收集的Region的集合,CSet裏的Region可以是任意代的。在GC的時候,對於old->young和old->old的跨代對象引用,只要掃描對應的CSet中的RSet即可。如果Rset集合的引用對象較多,這裏爲了提高引用對象的查找和賦值處理問題,又通過卡表(Card Table)來實現查詢和賦值,一個Card Table將一個分區在邏輯上劃分爲固定大小的連續區域,每個區域稱之爲卡。卡通常較小,介於128到512字節之間。RSet其實是一個Hash Table,Key是別的調用方Region的起始地址,Value是一個集合,裏面的元素是Card Table的Index分區的地址。如下圖9所示:
如上圖所示,要回收年輕代的region A,只需要掃描C,D,F 區域的根對象即可,而不需要掃描整個old區。
分代G1模式下選擇CSet有兩種子模式,分別對應YoungGC和mixedGC:
- YoungGC:CSet就是所有年輕代裏面的Region;
- MixedGC:CSet是所有年輕代裏的Region加上在全局併發標記階段標記出來的收益高的Region;
2、如何解決對象在GC過程中分配的問題呢?
初始快照算法:snapshot-at-the-beginning (SATB),SATB是維持併發GC的一種手段。G1併發的基礎就是SATB。SATB可以理解成在GC開始之前對堆內存裏的對象做一次快照,此時活的對象就認爲是活的,從而形成一個對象圖。在GC收集的時候,新產的對象認爲是活的對象,除此之外其他不可達的對象都認爲是垃圾對象。
如何找到在GC的過程中分配的對象呢?每個region記錄着兩個top-at-mark-start(TAMS)指針,分別爲prevTAMS和nextTAMS。在TAMS以上的對象就是新分配的,因而被視爲隱式marked。通過這種方式我們就找到了在GC過程中新分配的對象,並把這些對象認爲是活的對象。
3、在GC過程中引用發生變化的問題怎麼解決呢?
三色標記算法
在併發標記中,通過三色標記法來完成對對象是否存活以及追蹤的記錄。比如我們定義三種顏色並賦予以下的意義:
- 黑色:根對象,或者該對象與它的子對象都被掃描
- 灰色:對象本身被掃描,但還沒掃描完該對象中的子對象
- 白色:未被掃描對象,掃描完成所有對象之後,最終爲白色的爲不可達對象,即垃圾對象;
如果在GC運行中,對象的引用關係發生來如下的變化
如圖11所示,gc運行過程中,C對象的引用關係發生來改變,D引用C顯然按照三色標記法C爲白色是要被清理的,顯然不太合理。所以這裏需要記錄此種改變。
在CMS採用的是增量更新(Incremental update),只要在寫屏障(write barrier)裏發現要有一個白對象的引用被賦值到一個黑對象 的字段裏,那就把這個白對象變成灰色的,即插入的時候記錄下來。
write_barrier(obj,field,newobj){
if(newobj.mark == FALSE){
newobj.mark = TRUE
push(newobj,$mark_stack)
}
*field = newobj
}
在G1中,通過Write Barrier就可以瞭解到哪些引用對象發生了什麼樣的變化,刪除的時候記錄所有的對象,它有3個步驟:
(1) 在開始標記的時候生成一個快照圖SATB標記存活對象;
(2) 在併發標記的時候所有被改變的對象入隊(在write barrier裏把所有舊的引用所指向的對象都變成非白的);
(3) 可能存在遊離的垃圾,將在下次被收集;
4、可預測的停頓
G1記錄跟蹤了各個Region獲取垃圾收集的價值大小,在後臺維護一個優先列表;每次根據用戶設置的允許的收集時間,優先回收價值最大的Region,可以有計劃的避免全區的垃圾收集,這也是Garbage-First的由來,也是與CMS最大的區別;這就保證了在有限的時間內可以獲取儘可能高的收集效率;
5、什麼情況下會發生fullgc?
導致CMS FullGC的原因有兩個:
1. Promotion Failure
在年輕代晉升的時候老年代沒有足夠的連續空間容納,很有可能是內存碎片導致的。
2. Concurrent Mode Failure
在併發過程中jvm覺得在併發過程結束前堆就會滿了,需要提前觸發Full GC。
導致G1 Full GC的原因可能有兩個,與CMS類似:
1. Evacuation的時候沒有足夠的to-space來存放晉升的對象;
2. 併發處理過程完成之前空間耗盡
G1的初衷就是要避免Full GC的出現,Full GC會會對所有region做Evacuation-Compact,而且是單線程的STW,非常耗時間。
其他:
system.gc()調用;
永久代空間不足,注意動態代理,反射,常量等用的比較多的服務;
6、Full-GC的影響?
- 服務體驗:服務停止,嚴重影響用戶的體驗;
- 數據一致性:分佈式服務中容易引起主備切換,出現腦裂;
注意點
(1)不斷調優暫停時間指標
通過XX:MaxGCPauseMillis=x可以設置啓動應用程序暫停的時間,G1在運行的時候會根據這個參數選擇CSet來滿足響應時間的設置。一般情況下這個值設置到100ms或者200ms都是可以的(不同情況下會不一樣),但如果設置成50ms就不太合理。暫停時間設置的太短,就會導致出現G1跟不上垃圾產生的速度,最終退化成Full GC。所以對這個參數的調優是一個持續的過程,逐步調整到最佳狀態。
(2)不要設置新生代和老年代的大小
G1收集器在運行的時候會調整新生代和老年代的大小。通過改變代的大小來調整對象晉升的速度以及晉升年齡,從而達到我們爲收集器設置的暫停時間目標。設置了新生代大小相當於放棄了G1爲我們做的自動調優。我們需要做的只是設置整個堆內存的大小,剩下的交給G1自己去分配各個代的大小。
G1的運行過程是這樣的,會在Young GC和Mix GC之間不斷的切換運行,同時定期的做全局併發標記,在實在趕不上回收速度的情況下使用Full GC(Serial GC)。初始標記是搭在YoungGC上執行的,在進行全局併發標記的時候不會做Mix GC,在做Mix GC的時候也不會啓動初始標記階段。當MixGC趕不上對象產生的速度的時候就退化成Full GC,這一點是需要重點調優的地方。
優點 |
G1主要用來取代CMS垃圾收集器,優點是: 1、簡單:開發者控制調優變的簡單; 2、並行與併發:充分利用cpu,減少STW的時間; 3、可預見性:可預測的停頓模型,G1可選取部分區域進行回收,可以縮小回收範圍,控制減少全局停頓; 5、劃分模式:堆內存的劃分Region; 6、大空間分配:超大堆的表現更出色; |
缺點 | 會產生3次STW,但是時間較短; |
使用場景 | Java 9 的默認垃圾收集器,該收集器和之前的收集器大不相同,該收集器可以工作在young 區,也可以工作在 old 區。 |
參數使用 |
|
CMS與G1的區別:
(1) 分代收集 重新標記過程 和 回收方式(可預測回收模型)的不同。 |
總結
參數的設定及垃圾回收器的選擇一定要根據具體的服務及場景來判斷選擇,沒有完美的唯一解決方案。比如C端服務器交互密集型,需要保證吞吐量,我們可以選擇G1 或者 “吞吐量優先”的 Parallel Scavenge + Parallel Old,還有
PreNew + CMS的組合也是系統常用的,兼顧了吞吐量和停頓時間的性能考慮,
或者是:
Parallel Scavenge + old serial
,對於一般請求量不大,不要求實時性的單核cpu系統也可以採用 Serial + Serial old 即可滿足需求,效率也很高。
備註:常用參數
JVM常用配置參數
配置參數 | 功能 |
---|---|
-Xms | 初始堆大小。如:-Xms4g |
-Xmx | 最大堆大小。如:-Xmx4g |
-Xmn | 新生代大小。通常爲 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 個 Survivor 空間。實際可用空間爲 = Eden + 1 個 Survivor,即 90% |
-Xss | JDK1.5+ 每個線程堆棧大小爲 1M,一般來說如果棧不是很深的話, 1M 是絕對夠用了的。 |
-XX:NewRatio | 新生代與老年代的比例,如 –XX:NewRatio=2,則新生代佔整個堆空間的1/3,老年代佔2/3 |
-XX:SurvivorRatio | 新生代中 Eden 與 Survivor 的比值。默認值爲 8。即 Eden 佔新生代空間的 8/10,另外兩個 Survivor 各佔 1/10 |
-XX:PermSize | 永久代(方法區)的初始大小 |
-XX:MaxPermSize | 永久代(方法區)的最大值 |
GC日誌打印參數參考
gc日誌打印參數 |
|
參考資料:
《深入瞭解jvm虛擬機》
https://blog.csdn.net/qq_31156277/article/details/79962445
https://www.cnblogs.com/yang-hao/p/5936059.html
https://www.cnblogs.com/ASPNET2008/p/6496481.html
https://www.cnblogs.com/yunxitalk/p/8987318.html
https://blog.csdn.net/qq_31156277/article/details/79951819
https://blog.csdn.net/hutongling/article/details/69908443