面試官:要不這次來聊聊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面試,對線面試官系列持續更新中!
【對線面試官-移動端】系列 一週兩篇持續更新中!
【對線面試官-電腦端】系列 一週兩篇持續更新中!
原創不易!!求三連!!