在闡述三種垃圾收集器以前,先普及下幾種垃圾回收算法
①、引用計數算法:通過對象被引用的次數確定對象是否被使用,缺點是無法解決循環引用的問題。
②、復制算法:分為from塊和to塊,開始在from塊,回收時將from塊存活的對象復制到to塊,將from塊清空,to塊變from塊,from塊變to塊,缺點是內存使用率較低。
③、標記清除算法:分為標記對象和標記不在使用的對象兩個階段,缺點是會產生內存碎片。
④、標記整理算法:與標記清除算法相同,不過在清楚后會進行內存整理。
⑤、分代回收算法:當前的商業虛擬機的垃圾收集都是采用“分代收集”(Generational Collection)算法,這種算法並沒有什么新的思想,只是根據對象存活周期的不同將內存划分為幾塊。一般是把堆划分為新生代和老年代,這樣就可以根據各個年代的特點采用最適合的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就采用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或者“標記-整理”算法來進行回收。
如上圖,Eden用於分配新的內存空間,當第一輪回收后剩下的對象被放到Survivor1,此時年齡為1。第二次剩下的對象則年齡為1,第一次的年齡為2,他們被復制到Survivor2。當再次回收的時候,又由Survivor2轉換到Survivor1容器,他們兩個反復替換。當對象的年齡成長到8以后,被移動到老年代。永久代又叫方法區。
Minor GC 年輕代的回收
Major GC 年老代的回收
jvm提供的年輕代回收算法屬於復制算法,CMS、G1,ZGC屬於標記清除算法。
一、CMS收集器
Concurrent Mark Sweep,以獲取最短回收停頓時間為目標的收集器,基於並發“標記清理”實現。JDK1.7之前的默認垃圾回收算法,並發收集,停頓小。
優點:
並發,低停頓
缺點:
1、對CPU非常敏感:在並發階段雖然不會導致用戶線程停頓,但是會因為占用了一部分線程使應用程序變慢
2、無法處理浮動垃圾:在最后一步並發清理過程中,用戶縣城執行也會產生垃圾,但是這部分垃圾是在標記之后,所以只有等到下一次gc的時候清理掉,這部分垃圾叫浮動垃圾
3、CMS使用“標記-清理”法會產生大量的空間碎片,當碎片過多,將會給大對象空間的分配帶來很大的麻煩,往往會出現老年代還有很大的空間但無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次FullGC,為了解決這個問題CMS提供了一個開關參數,用於在CMS頂不住,要進行FullGC時開啟內存碎片的合並整理過程,但是內存整理的過程是無法並發的,空間碎片沒有了但是停頓時間變長了
過程:
1、初始標記:獨占PUC,僅標記GCroots能直接關聯的對象
補充點:GCroots能直接關聯的4種對象
①、虛擬機棧中引用的對象;②、方法區中類靜態屬性引用的對象;③、方法區中常量引用的對象;④、本地方法棧JNI(native方法)引用的對象
2、並發標記:可以和用戶線程並行執行,標記所有可達對象
3、重新標記:獨占CPU(STW),對並發標記階段用戶線程運行產生的垃圾對象進行標記修正
4、並發清理:可以和用戶線程並行執行,清理垃圾
CMS 出現FullGC的原因:
1、年輕帶晉升到老年帶沒有足夠的連續空間,很有可能是內存碎片導致的
2、在並發過程中JVM覺得在並發過程結束之前堆就會滿,需要提前觸發FullGC
二、G1收集器
Garbage First,是一款面向服務端應用的垃圾收集器。G1算法JDK1.9之后默認回收算法,特點是保持高回收率的同時,減少停頓。
特點:
1、並行於並發:G1能充分利用CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短stop-The-World停頓時間。部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過並發的方式讓java程序繼續執行。
2、分代收集:分代概念在G1中依然得以保留。雖然G1可以不需要其它收集器配合就能獨立管理整個GC堆,但它能夠采用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。也就是說G1可以自己管理新生代和老年代了。
3、空間整合:由於G1使用了獨立區域(Region)概念,G1從整體來看是基於“標記-整理”算法實現收集,從局部(兩個Region)上來看是基於“復制”算法實現的,但無論如何,這兩種算法都意味着G1運作期間不會產生內存空間碎片。
4、可預測的停頓:這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用這明確指定一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。
如果不計算維護Remembered Set的操作,G1收集器的運作大致可划分為以下幾個步驟:
1、初始標記(Initial Making)
2、並發標記(Concurrent Marking)
3、最終標記(Final Marking)
4、篩選回收(Live Data Counting and Evacuation)
看上去跟CMS收集器的運作過程有幾分相似,不過確實也這樣。初始階段僅僅只是標記一下GC Roots能直接關聯到的對象,並且修改TAMS(Next Top Mark Start)的值,讓下一階段用戶程序並發運行時,能在正確可以用的Region中創建新對象,這個階段需要停頓線程,但耗時很短。並發標記階段是從GC Roots開始對堆中對象進行可達性分析,找出存活對象,這一階段耗時較長但能與用戶線程並發運行。而最終標記階段需要吧Remembered Set Logs的數據合並到Remembered Set中,這階段需要停頓線程,但可並行執行。最后篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計划,這一過程同樣是需要停頓線程的,但Sun公司透露這個階段其實也可以做到並發,但考慮到停頓線程將大幅度提高收集效率,所以選擇停頓。
與其它收集器相比,G1變化較大的是它將整個Java堆划分為多個大小相等的獨立區域(Region),雖然還保留了新生代和來年代的概念,但新生代和老年代不再是物理隔離的了它們都是一部分Region(不需要連續)的集合。同時,為了避免全堆掃描,G1使用了Remembered Set來管理相關的對象引用信息。當進行內存回收時,在GC根節點的枚舉范圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏了。
三、ZGC收集器
Z Garbage Collector 垃圾收回器,也被稱為 ZGC, 是一種可伸縮的低延遲垃圾收集器。Java 11包含一個全新的垃圾收集器--ZGC,它由Oracle開發。
目標
垃圾回收停頓時間不超過10ms
無論是相對小的堆(幾百MB)還是大堆(TB級)都能應對自如
與G1相比,吞吐量下降不超過15%
方便日后在此基礎上實現新的gc特性、利用colored pointers(譯者注:暫時翻譯為彩色指針)和讀屏障進一步優化收集器
ZGC描述
大體上來說,ZGC是一種並發的、不分代的、基於Region且支持NUMA的壓縮收集器。因為只會在枚舉根節點的階段STW, 因此停頓時間不會隨着堆大小或存活對象的多少而增加。
ZGC的一個核心設計就是讀屏障與彩色指針(colored object pointers, 縮寫, colored oops)組合起來使用總體來說是一種利用64位指針中未使用的bit來保存元數據的指針)。這是ZGC可以與用戶線程並發執行的原因。從Java的線程角度來看,讀取Java對應中的引用變量的操作屬於一種讀屏障。與單純的取對象內存地址相比,使用讀屏障可以利用彩色指針中包含的信息來決定在允許Java線程讀取指針的地址值之前是否需要執行一些操作。例如,對象可能被垃圾收集器移動過了,這時讀屏障就可以感知到這種情況並執行一些必要的行為。
跟其它的可選方案相比,我們認為使用彩色指針模式有一些非常吸引人的優勢,比如:
– 這允許我們在移動對象/整理內存階段,在指向可回收/重用區域的指針確定之前回收/重用這部分內存。(原文: It allows us to reclaim and reuse memory during the relocation/compaction phase, before pointers pointing into the reclaimed/reused regions have been fixed.)。這有利於降低堆的開銷。這同時也意味着我們不需要再實現一個單獨的標記-整理算法用於處理Full GC。
– 這允許我們使用相對來說更少量、更簡單的GC屏障。這可以降低JVM運行時的性能開銷。同時也可以讓JVM字節碼解釋器和JIT編譯器中的GC代碼更加容易實現和優化。
– 我們目前會在彩色指針中保存與標記和重定位相關的數據。不過,只要彩色指針中還有足夠的未使用的bit, 我們還可以在里面存儲更多對讀屏障有用的信息。我們認為這為未來實現更多的特性奠定了良好的基礎。比如,在復雜多變的內存環境下,我們可以在彩色指針中存儲一些追蹤信息來讓垃圾回收器在移動對象時能將低頻次使用的對象移動到不常訪問的內存區域。