面試官:要不這次來聊聊G1垃圾收集器?
候選者:嗯嗯,好的呀
候選者:上次我記得說過,CMS垃圾收集器的弊端:會産生記憶體碎片&&空間需要預留
候選者:這倆個問題在處理的時候,很有可能會導緻停頓時間過長,說白了就是CMS的停頓時間是「不可預知的」
候選者:而G1又可以了解為在CMS垃圾收集器上進行"更新"
候選者:G1 垃圾收集器可以給你設定一個你希望Stop The Word 停頓時間,G1垃圾收集器會根據這個時間盡量滿足你
候選者:在前面我在介紹JVM堆的時候,是畫了一張圖的。堆的記憶體分布是以「實體」空間進行隔離

候選者:在G1垃圾收集器的世界上,堆的劃分不再是「實體」形式,而是以「邏輯」的形式進行劃分
候選者:不過,像之前說過的「分代」概念在G1垃圾收集器的世界還是一樣奏效的
候選者:比如說:新對象一般會配置設定到Eden區、經過預設15次的Minor GC新生代的對象如果還存活,會移交到老年代等等...
候選者:我來畫下G1垃圾收集器世界的「堆」空間分布吧
候選者:從圖上就可以發現,堆被劃分了多個同等份的區域,在G1裡每個區域叫做Region
候選者:老年代、新生代、Survivor這些應該就不用我多說了吧?規則是跟CMS一樣的
候選者:G1中,還有一種叫 Humongous(大對象)區域,其實就是用來存儲特别大的對象(大于Region記憶體的一半)
候選者:一旦發現沒有引用指向大對象,就可直接在年輕代的Minor GC中被回收掉
面試官:嗯...
候選者:其實稍微想一下,也能了解為什麼要将「堆空間」進行「細分」多個小的區域
候選者:像以前的垃圾收集器都是對堆進行「實體」劃分
候選者:如果堆空間(記憶體)大的時候,每次進行「垃圾回收」都需要對一整塊大的區域進行回收,那收集的時間是不好控制的
候選者:而劃分多個小區域之後,那對這些「小區域」回收就容易控制它的「收集時間」了
面試官:那我大概了解了。那要不你講講它的GC過程呗?
候選者:嗯,在G1收集器中,可以主要分為有Minor GC(Young GC)和Mixed GC,也有些特殊場景可能會發生Full GC
候選者:那我就直接說Minor GC先咯?
面試官:嗯,開始吧
候選者:G1的Minor GC其實觸發時機跟前面提到過的垃圾收集器都是一樣的
候選者:等到Eden區滿了之後,會觸發Minor GC。Minor GC同樣也是會發生Stop The World的
候選者:要補充說明的是:在G1的世界裡,新生代和老年代所占堆的空間是沒那麼固定的(會動态根據「最大停頓時間」進行調整)
候選者:這塊要知道會給我們提供參數進行配置就好了
候選者:是以,動态地改變年輕代Region的個數可以「控制」Minor GC的開銷
面試官:嗯,那Minor GC它的回收過程呢?可以稍微詳細補充一下嗎
候選者:Minor GC我認為可以簡單分為為三個步驟:根掃描、更新&&處理 RSet、複制對象
候選者:第一步應該很好了解,因為這跟之前CMS是類似的,可以了解為初始标記的過程
候選者:第二步涉及到「Rset」的概念
候選者:從上一次我們聊CMS回收過程的時候,同樣講到了Minor GC,它是通過「卡表」(cart table)來避免全表掃描老年代的對象
候選者:因為Minor GC 是回收年輕代的對象,但如果老年代有對象引用着年輕代,那這些被老年代引用的對象也不能回收掉
候選者:同樣的,在G1也有這種問題(畢竟是Minor GC)。CMS是卡表,而G1解決「跨代引用」的問題的存儲一般叫做RSet
候選者:隻要記住,RSet這種存儲在每個Region都會有,它記錄着「其他Region引用了目前Region的對象關系」
候選者:對于年輕代的Region,它的RSet 隻儲存了來自老年代的引用(因為年輕代的沒必要存儲啊,自己都要做Minor GC了)
候選者:而對于老年代的 Region 來說,它的 RSet 也隻會儲存老年代對它的引用(在G1垃圾收集器,老年代回收之前,都會先對年輕代進行回收,是以沒必要儲存年輕代的引用)
候選者:那第二步看完RSet的概念,應該也好了解了吧?
候選者:無非就是處理RSet的資訊并且掃描,将老年代對象持有年輕代對象的相關引用都加入到GC Roots下,避免被回收掉
候選者:到了第三步也挺好了解的:把掃描之後存活的對象往「空的Survivor區」或者「老年代」存放,其他的Eden區進行清除
候選者:這裡要提下的是,在G1還有另一個名詞,叫做CSet。
候選者:它的全稱是 Collection Set,儲存了一次GC中「将執行垃圾回收」的Region。CSet中的所有存活對象都會被轉移到别的可用Region上
候選者:在Minor GC 的最後,會處理下軟引用、弱引用、JNI Weak等引用,結束收集
面試官:嗯,了解了,不難
面試官:我記得你前面提到了Mixed GC ,要不來聊下這個過程呗?
候選者:好,沒問題的。
候選者:當堆空間的占用率達到一定門檻值後會觸發Mixed GC(預設45%,由參數決定)
候選者:Mixed GC 依賴「全局并發标記」統計後的Region資料
候選者:「全局并發标記」它的過程跟CMS非常類型,步驟大概是:初始标記(STW)、并發标記、最終标記(STW)以及清理(STW)
面試官:确實很像啊,你繼續來聊聊具體的過程呗?
候選者:嗯嗯,還是想說明下:Mixed GC它一定會回收年輕代,并會采集部分老年代的Region進行回收的,是以它是一個“混合”GC。
候選者:首先是「初始标記」,這個過程是「共用」了Minor GC的 Stop The World(Mixed GC 一定會發生 Minor GC),複用了「掃描GC Roots」的操作。
候選者:在這個過程中,老年代和新生代都會掃
候選者:總的來說,「初始标記」這個過程還是比較快的,畢竟沒有追溯周遊嘛
面試官:...
候選者:接下來就到了「并發标記」,這個階段不會Stop The World
候選者:GC線程與使用者線程一起執行,GC線程負責收集各個 Region 的存活對象資訊
候選者:從GC Roots往下追溯,查找整個堆存活的對象,比較耗時
候選者:接下來就到「重新标記」階段,跟CMS又一樣,标記那些在「并發标記」階段發生變化的對象
候選者:是不是很簡單?
面試官:且慢
面試官:CMS在「重新标記」階段,應該會重新掃描所有的線程棧和整個年輕代作為root
面試官:據我了解,G1好像不是這樣的,這塊你了解嗎?
候選者:嗯,G1 确實不是這樣的,在G1中解決「并發标記」階段導緻引用變更的問題,使用的是SATB算法
候選者:可以簡單了解為:在GC 開始的時候,它為存活的對象做了一次「快照」
候選者:在「并發階段」時,把每一次發生引用關系變化時舊的引用值給記下來
候選者:然後在「重新标記」階段隻掃描着塊「發生過變化」的引用,看有沒有對象還是存活的,加入到「GC Roots」上
候選者:不過SATB算法有個小的問題,就是:如果在開始時,G1就認為它是活的,那就在此次GC中不會對它回收,即便可能在「并發階段」上對象已經變為了垃圾。
候選者:是以,G1也有可能會存在「浮動垃圾」的問題
候選者:但是總的來說,對于G1而言,問題不大(畢竟它不是追求一次把所有的垃圾都清除掉,而是注重 Stop The World時間)
候選者:最後一個階段就是「清理」,這個階段也是會Stop The World的,主要清點和重置标記狀态
候選者:會根據「停頓預測模型」(其實就是設定的停頓時間),來決定本次GC回收多少Region
候選者:一般來說,Mixed GC會標明所有的年輕代Region,部分「回收價值高」的老年代Region(回收價值高其實就是垃圾多)進行采集
候選者:最後Mixed GC 進行清除還是通過「拷貝」的方式去幹的
候選者:是以,一次回收未必是将所有的垃圾進行回收的,G1會依據停頓時間做出選擇Region數量(:
面試官:嗯,過程我大緻是了解了
面試官:那G1會什麼時候發生full GC?
候選者:如果在Mixed GC中無法跟上使用者線程配置設定記憶體的速度,導緻老年代填滿無法繼續進行Mixed GC,就又會降級到serial old GC來收集整個GC heap
候選者:不過這個場景相較于CMS還是很少的,畢竟G1沒有CMS記憶體碎片這種問題(:
本文總結(G1垃圾收集器特點):
從原來的「實體」分代,變成現在的「邏輯」分代,将堆記憶體「邏輯」劃分為多個Region
使用CSet來存儲可回收Region的集合
使用RSet來處理跨代引用的問題(注意:RSet不保留 年輕代相關的引用關系)
G1可簡單分為:Minor GC 和Mixed GC以及Full GC
【Eden區滿則觸發】Minor GC 回收過程可簡單分為:(STW) 掃描 GC Roots、更新&&處理Rset、複制清除
【整堆空間占一定比例則觸發】Mixed GC 依賴「全局并發标記」,得到CSet(可回收Region),就進行「複制清除」
R大描述G1原理的時候,從宏觀的角度看G1其實就是「全局并發标記」和「拷貝存活對象」
使用SATB算法來處理「并發标記」階段對象引用可能會修改的問題
提供可停頓時間參數供使用者設定(G1會盡量滿足該停頓時間來調整 GC時回收Region的數量)
歡迎關注我的微信公衆号【Java3y】來聊聊Java面試,對線面試官系列持續更新中!
【對線面試官-移動端】系列 一周兩篇持續更新中!
【對線面試官-電腦端】系列 一周兩篇持續更新中!
原創不易!!求三連!!
更多的文章可往:文章的目錄導航