jvm垃圾回收演算法
前言
java相較於c、c++語言的優勢之一是自帶垃圾回收器,程式開發人員不用手動管理記憶體,記憶體的分配和釋放完全由gc(Garbage Collector)來做,極大地提高了軟體開發效率及程式健壯性(手動管理記憶體容易造成記憶體洩漏)。凡事皆有兩面性,java gc在給我們帶來記憶體管理便捷性的同時,也面臨STW(Stop The World)影響程式吞吐的缺陷。作為java開發人員,只有深入理解jvm垃圾回收的機制,才能在程式效能出現瓶頸時,更好的對程式進行優化。筆者通過拜讀《深入理解java虛擬機器》,總結書中內容,給大家分享下java虛擬機器常見的垃圾回收演算法。
垃圾確定
在垃圾回收之前,jvm需要確定哪些物件已死,即需要當做垃圾被回收。垃圾確認的方法傳統的有引用計數法:用一個引用計數器來標記物件當前的引用次數,當引用計數為0時,物件可回收。這種方法有個弊端是無法解決迴圈引用的問題,如兩個物件相互引用則它們永遠不會釋放。另外一種方法是可達性分析演算法,目前主流的語言(java、c#、golang等)都是採用這種方法來判定一個物件是否存活。可達性分析演算法的思路是:將一系列根物件作為起點,沿著這些起點向下搜尋,搜尋經過的所有路徑成為引用鏈,沒有在引用鏈中的物件則為不可達物件,這類物件會被判斷為可回收物件。可達性分析圖如下:
可達性分析
標記-清除演算法(Mark-Sweep)
標記清除演算法是最基礎的收集演算法,顧名思義,演算法分為兩個階段:首先標記需要回收的物件,然後統一回收所有被標記的物件。這種垃圾回收演算法比較簡單,但是存在兩個不足之處:一個是效率問題,標記和清除都需要掃描整個記憶體空間的物件,效率較低;另一個是標記清除後會產生大量的記憶體碎片。標記-清除演算法流程如下所示:
標記-清除演算法
複製演算法
為了解決上面的效率和記憶體碎片問題,有人提出了“複製”演算法,它的思路是將記憶體一分為二,每次分配物件時只使用其中一半記憶體空間,當一塊記憶體空間用完時,就將該塊記憶體空間上存活的物件複製到另外一塊記憶體上,然後將已使用過的記憶體空間一次清理掉,這樣每次只對一半的記憶體進行回收,同時也解決了記憶體碎片的問題。同樣,複製演算法也有很明顯的缺點:1. 犧牲了一半的記憶體空間,記憶體利用率最大也就50%;2. 當記憶體中存活的物件較多時,會進行大量的複製操作,效率會較低。複製演算法的執行過程如下所示:
複製演算法
標記-整理演算法(Mark-Compact)
複製演算法在存在大量存活物件的情況下效率低下,更關鍵的是50%的記憶體空間得不到利用,如果存活物件過多,記憶體空間可能不夠,則需要額外的空間作為擔保,因此,對於老年代不適合使用這種演算法(新生代有老年代做擔保)。
因此,根據老年代的特點,有人提出了“標記-整理”演算法,標記-整理演算法和標記-清除演算法的第一步相同,即對所有需要回收的物件進行標記,不同的是,標記-整理演算法在第二階段並不是直接對可回收的物件進行清理,而是將所有存活的物件向一端移動,然後一把清理掉存活邊界之外的記憶體。這種演算法結合了標記-清除和複製演算法的思想,即未浪費記憶體也避免了記憶體碎片的產生。“標記-整理”演算法如下所示:
標記-整理演算法
分代收集演算法
分代收集並沒有什麼創新之處,這種演算法只是將jvm堆記憶體分為不同的區域,再根據區域的特點採用不同的垃圾回收演算法。一般分為新生代和老年代,新生代在再細分為Eden和Survivor區(8:1),在新生代中,物件的存活率低,因此採用“複製”演算法,將Edian區存活的物件複製到Survivor區,而老年代中物件存活率高且沒有額外的中間做擔保,因此採用“標記-清除”或者“標記-整理”演算法來進行垃圾回收。
總結
本文拋磚引玉,介紹了目前主流的垃圾回收演算法的原理並未對主流的垃圾回收器的實現就行剖析,更詳細的細節可以研讀《深入理解java虛擬機器》。筆者後續將分享主流的jvm實現原理,同時歡迎大家多多交流。