上一篇文章學習的垃圾收集演算法是記憶體回收的方法論,我們這篇文章學習記憶體回收的具體實現垃圾收集器。
相關概念並行(Parallel):指多條垃圾收集執行緒並行工作,但是此時使用者執行緒仍然處於等待狀態。
併發(Cocurrent):指使用者執行緒與垃圾收集執行緒同時執行,不一定是並行的,可能會存在交替執行,此時使用者執行緒繼續執行,垃圾收集執行緒運行於另一個CPU上。
列舉根節點:可達性分析中GC Roots的節點主要在全域性引用和執行上下文中,可達性分析在整個分析期間整個系統必須是停止的,導致GC過程中必須停頓所有Java執行執行緒(Stop The World後續稱為STW),當系統停止後,並不需要一個不漏的檢查完所有執行上下文和全域性引用的位置,Java虛擬機器使用一組稱為OopMap的資料結構來存放物件的引用。這樣GC掃描時就可以直接得到資訊了。
安全點(Safepoint):在特定的位置為指令生成OopMap,這個特定位置就稱為安全點。在程式執行期間並非所有地方都停頓下來開始GC,只有達到安全點時才可以暫停。現在都採用主動式中斷思想來暫停執行緒來響應GC事件。
安全區域:指在一段程式碼中,引用關係不會發生變化,在這個區域中任何地方開始GC都是安全的。
垃圾收集器在HotSpot中,記憶體回收如何進行是由虛擬機器所採用的垃圾收集器來決定的,不同廠商不同版本的虛擬機器所提供的垃圾收集器可能是有很大的差別,我們今天學習的垃圾收集器是基於HotSpot虛擬機器的。
我們先透過一個圖片瞭解下這個虛擬機器中包含的所有收集器:
上圖中展示了7種作用於不同分代的收集器,兩個收集器之間的連線表示它們可以搭配使用,收集器的選擇需要具體的場景具體分析,沒有最好的收集器。
Serial收集器Serial收集器是一個單執行緒收集器,它只會使用一個CPU或者一個GC執行緒去完成垃圾收集工作,更主要的是在它進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束。STW是由虛擬機器在後臺自動發起和自動完成的,在使用者不可見的情況下把使用者的工作執行緒全部暫停,對使用者來說是無法接受的。
優點:簡單而高效(與其他收集器的單執行緒比),對於限定的單個CPU來說,Serial收集器由於沒有執行緒互動的開銷,專心垃圾收集可以獲得最高的單執行緒收集效率。
在使用者的桌面應用場景中,分配給虛擬機器管理的記憶體一般不會很大,收集幾十兆甚至一百兆的新生代停頓時間可以控制在幾十毫秒最多一百毫秒以內,只要不是頻繁發生還是可以接受的。所以,Serial收集器對於執行在Client模式下的虛擬機器來說還算是一個比較好的選擇。
Serial/Serial Old收集器的工作過程:
ParNew收集器ParNew收集器是Serial收集器的多執行緒版本,除了使用多執行緒進行垃圾回收之外,其他行為包括引數設定、收集演算法、STW、物件分配規則、回收策略等都是與Serial收集器完全一樣的。
優點:除了Serial收集器之外,唯一可以和CMS收集器配合工作的收集器。隨著CPU數量的增加,它的回收效率還是比較高的。但是在單執行緒環境下,由於執行緒互動的開銷,ParNew收集器的效率並不會比Serial收集器高。
ParNew預設開啟的收集執行緒數與CPU數量相等。
ParNew/Serial Old收集器的工作過程:
Parallel Scavenge收集器Parallel Scavenge收集器是一個新生代收集器,它也是使用複製演算法的收集器,又是並行的多執行緒收集器,是不是看起來跟ParNew收集器一樣的,但是Parallel Scavenge收集器關注點是達到一個可控制的吞吐量,而不是像其他收集器不管怎麼最佳化都是為了減少STW的時間。
吞吐量是CPU執行使用者程式碼的時間與CPU總消耗時間的比值,即吞吐量 = 執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集時間)。
停頓時間短的收集器比較適合與使用者互動的程式,而高吞吐量則可以更高效的利用CPU時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務。
Parallel Scavenge收集器的工作過程可以參考ParNew收集器。
Serial Old收集器Serial Old收集器是Serial收集器的老年代版,它也是一個單執行緒收集器,使用標記-整理演算法。
Serial Old收集器可以與Parallel Scavenge收集器搭配使用,還可以作為CMS收集器的後備方案,在併發收集發生Cocurrent Mode Failure時使用。
Serial/Serial Old收集器的工作過程:
Parallel Old收集器Parallel Old收集器是Parallel Scavenge收集器的老年代版,使用多執行緒和標記-整理演算法。
由於之前Parallel Scavenge收集器只能配合Serial Old收集器工作,受到了Serial Old在效能的拖累,有了Parallel Old收集器,在吞吐量優先的場景下就有了比較好的選擇。
Parallel Scavenge / Parallel Old收集器工作過程:
CMS收集器CMS(Concurrent Mark Sweep)收集器是一種以獲取最短停頓時間為目標的收集器。它是基於標記-清除演算法實現的,整個過程分為4個步驟:
初始標記:僅僅只是標記一下GC Roots能直接關聯到的物件,速度很快。併發標記:進行GC Roots Tracing的過程。重新標記:為了修正併發標記期間因使用者程式繼續執行而標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長,但是遠比並發標記階段短。併發清除:清除標記的物件,與使用者執行緒一起工作。其中初始標記、重新標記兩個步驟還是需要STW。
由於整個過程耗時最長的併發標記和併發清除過程都是與使用者執行緒一起工作,所以總體可以說CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。
CMS收集器工作過程:
CMS收集器雖然被稱為併發低停頓收集器,但是它有3個明顯的缺點:
CMS收集器對CPU資源非常敏感。CMS收集器預設啟動的回收執行緒數是(CPU數量 + 3)/ 4,當CPU數量比較小時,CMS收集器對使用者程式的影響就可能變得很大。比如CPU數量為2時,回收執行緒數佔用了1個CPU,等於分出了一半的運算能力去執行收集器執行緒,使用者程式的執行速度忽然降低了50%,比較讓人難以接受。CMS收集器無法處理浮動垃圾。由於CMS在併發清理階段使用者執行緒還在執行,在這個過程中自然還會有新的垃圾產生,這一部分垃圾出現在標記過程之後,CMS收集器無法在當此收集中處理掉他們,只能下次GC時再回收。這部分垃圾稱為浮動垃圾。也是由於在垃圾收集階段使用者程式還在執行,那麼就需要預留足夠的記憶體空間給使用者執行緒使用,因此CMS收集器不能等到老年代幾乎填滿才進行收集,而是需要預留空間供使用者程式使用。如果CMS執行期間的記憶體無法滿足使用者程式,就會出現Cocurrent Mode Failure失敗,此時虛擬機器將啟動預備方案:臨時啟動Serial Old收集器來進行老年代的垃圾收集,這樣反而停頓時間長了。CMS收集器採用標記-清除演算法,在收集結束後會產生大量的空間碎片。空間碎片過多時,將會給大物件分配帶來麻煩,往往還有足夠的記憶體空間,但是因為不連續而不滿足物件分配而不得不提前進行一次Full GC。為了解決這個問題,CMS收集器提供了引數在進行Full GC時開啟記憶體記憶體碎片的合併整理過程,並且還提供了引數設定多少次不壓縮之後執行一次壓縮的Full GC。虛擬機器引數的我們後面統一學習吧,勾勾一直記不住,每次用都是查資料,所以會抽時間把常用引數整理一下。G1收集器G1收集器是一款面向服務端應用的垃圾收集器,是當今收集器技術發展最前沿的技術成果之一。
與其他GC收集器相比,G1具備如下特點:
並行與併發:G1能充分利用CPU、多核環境下的硬體優勢,使用多個CPU來縮短STW停頓時間。部分其他收集器原本需要停頓Java執行緒執行的GC動作,G1收集器仍然可以透過併發的方式讓java程式繼續執行。分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。它能夠採用不同的方式去處理新建立的物件和已經存活了一段時間,熬過多次GC的舊物件以獲取更好的收集效果。空間整合:與CMS的標記-清理演算法不同,G1從整體來看是基於標記-整理演算法實現的收集器;從區域性上來看是基於複製演算法實現的。從整體看G1執行期間不會產生記憶體碎片,收集後能提供規整的可用記憶體。可預測的停頓:這是G1相對於CMS的另一個大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集的時間不超過N毫秒。在G1之前的垃圾收集進行收集的範圍是整個新生代或者老年代,而G1不再是這樣的。
它將整個堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但是新生代和老年代不再是物理隔離了,他們都是一部分Region不需要連續的集合。
G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region,因此G1能夠建立可預測的停頓時間模型,並且能夠獲得儘可能高的收集效率。
G1收集器的運作步驟可以分為以下幾個步驟:
初始標記:僅僅只是標記一下GC Roots能直接關聯到的物件,並且修改TAMS(Next Top at Mark Start)的值,讓下一個階段使用者程式併發執行時,能在正確可用的Region中建立新物件,這一階段需要停頓執行緒,但是耗時很短。併發標記:從GC Root開始對堆中物件進行可達性分析,找出存活的物件,這階段時耗時較長,但可與使用者程式併發執行。最終標記:為了修正在併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機器將這段時間物件變化記錄線上程Remenbered Set Logs裡面,最終標記階段需要把Remembered Set Logs的資料合併到Remembered Set 裡面,這一階段需要停頓執行緒,但是可並行執行。篩選回收:首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃。G1收集器工作過程:
關於G1收集器的使用,如果你現在使用的收集器沒有問題,那就沒必要選擇G1。如果應用追求高吞吐,G1也不是一個好的選擇,如果應用追求低停頓,可以考慮G1。
總結新生代垃圾收集器Serial、ParNew、Parallel Scavenge。
老年代垃圾收集器Serial Old、CMS、Parallel Old。
G1可以在新生代和老年代回收垃圾。
除了Parallel Scavenge收集器關注點不是停頓時間,其他垃圾收集器都是在Serial的基礎上最佳化而來,意在降低停頓時間,提高使用者體驗。
今天就到這裡了!
我們下篇文章見。
參考資料:《深入理解Java虛擬機器》