六種主要的垃圾回收演算法和思想

AmosH發表於2019-01-04

Java語言的一大特點就是可以自動進行垃圾回收處理,無需開發人員過於關注系統資源的釋放情況。自動垃圾收集雖然大大減輕了開發人員的工作量,但是也增加了軟體系統的負擔。一個不合適的垃圾回收方法和策略將會對系統效能造成不良影響。

1. 引用計數法

引用計數法是最經典古老的一種垃圾收集方法,它的實現也很簡單:對於一個物件A,只要有任何一個物件引用了A,則A的計數器就加1,當引用失效時,引用計數器就減1.只要物件A的引用計數器的值為0,則物件A就不可能再被使用。

引用計數法實現簡單,只需要為每一個物件配備一個整型計數器即可。但是,它存在一個很嚴重的問題,即無法處理迴圈引用的情況,因此在Java的垃圾回收器中沒有使用這種演算法。

一個簡單的迴圈引用示例如下:
<embed>

物件A和物件B迴圈引用,此時他們的引用計數器都不為0,但是在系統中已經找不到第三個物件引用了A或者B,也就是說,A和B應該是被回收的垃圾。但是因為迴圈引用而無法被識別,最終可能會導致記憶體洩漏。

2. 標記-清除法

標記-清除法是現代垃圾回收演算法的基礎。

它將垃圾回收分為兩個階段:標記階段和清除階段。一種可行的實現是:

  1. 在標記階段,標記所有從根節點出發的可達物件。因此,所有未被標記的物件就是未被引用的垃圾物件。
  2. 在清除階段,清除所有未被標記的物件。

而標記-清除法可能產生的最大問題就是空間碎片。回收之後的空間不是連續的,不連續的記憶體空間的工作效率要低於連續的空間,這是標記-清除法最大的缺點。

3.複製演算法

與標記-清除法相比,複製演算法是一種相對高效的回收演算法。

它的核心思想是:將記憶體分為兩部分,每次只使用其中一部分。在垃圾回收時,將正在使用的記憶體中的存貨物件複製到未使用的記憶體塊中,之後清除正在使用的記憶體塊中的所有物件,交換兩個記憶體的角色,完成垃圾回收。

如果系統中的垃圾物件很多,那麼複製演算法需要複製的存活物件的數量也不會很多,因此當需要使用複製演算法時還是比較高效的。又因為所有物件都會被統一複製到新的記憶體空間中,所以可以保證回收後的記憶體空間是沒有碎片的。

雖然有以上兩大有點,但是複製演算法的代價缺點是將系統記憶體摺半。因此單純的複製演算法會讓人無法接受。

在Java的新生代序列垃圾回收器中,使用了複製演算法。新生代分為eden空間、from空間和to空間三個部分。其中from和to空間可以視為用於複製的兩塊大小相同、地位相等,且角色可以互換的空間塊,它們也被稱為survivor空間,即倖存者空間,用於存放未被回收的物件。

在垃圾回收時,eden空間中的存活物件會被複制到未使用的survivor空間中(假設為to),正在使用的survivor空間(假設為from)中的年輕物件也會被複制到to空間中(大物件或者老年物件會直接進入老年代,如果to空間已經滿了,則物件也會直接進入老年代)。此時eden和from空間中的剩餘物件將都是垃圾物件,可以直接清空,to空間則存放此次回收後存活的物件。

這樣改進後的複製演算法既保證了空間的連續性,又避免大量記憶體空間被浪費。

4. 標記-壓縮演算法

複製演算法的高效性是建立在存活物件少,垃圾物件多的前提下。這種情況普遍存在於年輕代,但是在老年代,更常見的情況是大部分物件都是存活物件,如果使用複製演算法,由於存活物件多,複製的成本也會很高。

基於老年代垃圾回收的特性,需要使用新的演算法,而標記-壓縮演算法是老年代的一種回收演算法,它在標記-清除演算法之上做了一些優化。

它和標記-清除演算法不同之處在於:在清除階段,它會將所有的存活物件壓縮到記憶體的另一端。之後清理邊界之外的所有空間。這種演算法既避免了碎片的產生,又不需要兩塊相同的記憶體空間,因此價效比較高。

5. 增量演算法

對大部分的垃圾回收演算法而言,在垃圾回收的過程中,應用軟體的所有執行緒都會掛起,暫停一切正常工作,等待來回收的完成。如果垃圾回收時間很長,則應用程式會被掛起很久,這會嚴重影響使用者體驗和系統穩定性。

增量演算法的基本思想就是,讓垃圾回收執行緒和應用執行緒交替執行,每次只收集一小片區域的記憶體空間,接著切換應用程式執行緒。如此往復知道垃圾回收完成。

使用這種方式進行垃圾回收可以減少系統的停頓時間,但是因為執行緒切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。

6.分代

前面介紹的幾種垃圾回收演算法沒有哪一種可以完全替代其他演算法,它們有各自的優點和缺點。因此,根據垃圾回收物件的特性,使用合適的演算法回收,才是明智的選擇。

分代就是基於這種思想,它將記憶體區域根據物件特點分為幾塊,根據每塊記憶體區間的特點,使用不同的回收演算法,提高垃圾回收的效率。

相關文章