1. 程式人生 > >不積跬步,無以至千里;不積小流,無以成江海!

不積跬步,無以至千里;不積小流,無以成江海!

由於垃圾收集演算法在各個虛擬機器以及不同的平臺上會有不同的實現,所以開頭先大概講解一下幾個基本的演算法

1. 引用計數(Reference Counting)

為每一個物件新增一個計數器,計數器記錄了對該物件的活躍引用的數量。如果計數器為0,則說明這個物件沒有被任何變數所引用,即應該進行垃圾收集。
收集過程如下:
1. 減少被收集物件所引用的物件的計數器的值
2.將其放入延時收集佇列之中

引用計數的方法需要編譯器的配合。編譯器需要為此物件生成額外的程式碼。如賦值函式將此物件賦值給一個引用時,需要增加此物件的引用計數。還有就是,當一個引用變數的生命週期結束時,需要更新此物件的引用計數器。

引用計數的方法由於存在顯著的缺點,實際上並未被JVM所使用

。下面的程式可以用來證明這點:

public class ReferenceCountingGC {
	
	/**
	 * -verbose:gc -XX:+PrintGCDetails -Xms30M -Xmx30M -Xmn10M
	 */
	public static final int	    _1M	    = 1024 * 1024;
	private ReferenceCountingGC	ref	    = null;
	private byte[]	            content	= new byte [_1M * 1];
	
	public void testGC() {
		ReferenceCountingGC a = new ReferenceCountingGC();
		ReferenceCountingGC b = new ReferenceCountingGC();
		a.ref = a;
		b.ref = b;
		a = null;
		b = null;
		System.gc();
		
	}
	
	public static void main(String[] args) {
		new ReferenceCountingGC().testGC();
	}
	
}
下面為執行的結果:

[GC [PSYoungGen: 3411K->1248K(8960K)] 3411K->1296K(29440K), 0.0013584 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System) [
PSYoungGen: 1248K->0K(8960K)] [PSOldGen: 48K->1240K(20480K)] 1296K->1240K(29440K) [PSPermGen: 2993K->2993K(21248K)], 0.0064104 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Heap

從程式中可以看出,即使a,b兩個物件通過相互引用的方式來保持物件計數器的值不為零,但是任然會被垃圾回收器進行回收(

Java -verbose:gc 中引數-verbose:gc 表示輸出虛擬機器中GC的詳細情況.

執行結果解讀如下:

  箭頭前後的資料1248K和0K分別表示垃圾收集GC前後所有存活物件使用的記憶體容量,說明有1248K-0K=1248K的物件容量被回收,括號內的資料8960K為堆記憶體的總容量,收集所需要的時間是0.0064104秒(這個時間在每次執行的時候會有所不同)

Note:GC會暫用CPU時間片,有可能造成應用程式在某個時刻極短的停頓.

)

2.標記-清除收集器(Mark-Swap Collectors)
收集過程分為2個階段
1. 首先停止所有工作,從根集遍歷所有被引用的節點,然後進行標記,最後恢復所有工作
2. 收集階段會收集那些沒有被標記的節點,然後返回空閒連結串列

標記-清除法的缺點在於
1.標記階段暫停的時間可能很長,而整個堆在交換階段又是可訪問的,可能會導致被換頁換出記憶體。
2.另外一個問題在於,不管你這個物件是不是可達的,即是不是垃圾,都要在清楚階段被檢查一遍,非常耗時.

3,標記清楚這兩個動作會產生大量的記憶體碎片,於是當有大物件進行分配時,不需要觸發一次垃圾回收動作

3.拷貝收集器(Copying Collectors)(適用於young generation:PSYoungGen)

該演算法的提出是為了克服控制代碼的開銷和解決堆碎片的垃圾回收。
    將記憶體分為兩個區域(from space和to space)。所有的物件分配記憶體都分配到from space。在清理非活動物件階段,把所有標誌為活動的物件,copy到to space,之後清楚from space空間。然後互換from sapce和to space的身份。既原先的from space變成to sapce,原先的to space變成from space。每次清理,重複上述過程。

  現在的商業虛擬機器都用這種演算法來回收新生代,因為新生代的大多數的生命週期都很短暫,所以前面提到的兩塊相互切換的區域並不需要按照1:1來進行分配。而是分配了一個Eden區,兩個Survivor區。大部分物件預設的都是在 Eden區中生成。當垃圾回收時,Eden和其中的一個Survivor區的存活物件將被複制到另外一個Survivor區,當另外一個Survivor區也滿了的時候,從Eden和第一個Survivor區複製過來的並且此時還存活的物件,將被複制到tenured generation。需要注意,Survivor的兩個區是對稱的,沒先後關係,所以同一個區中可能同時存在從Eden複製過來物件,和從前一個Survivor複製過來的物件,而複製到年老區的只有從第一個Survivor去過來的物件。而且,Survivor區總有一個是空的。
    young generation的gc稱為minor gc。經過數次minor gc,依舊存活的物件,將被移出young generation,移到tenured generation

優點:copy演算法不理會非活動物件,copy數量僅僅取決為活動物件的數量。並且在copy的同時,整理了heap空間,即,to space的空間使用始終是連續的,記憶體使用效率得到提高。
缺點:預設情況下Eden:Survivor=8:1, 所以總會有100-(80+10)%的新生代記憶體會被浪費掉。

4.標記-整理收集器(Mark-Compact Collectors)(適用於存放生命週期較長物件的tenured generation:PSOldGen)
標記整理收集器,通過融合了標記-清除收集器和拷貝收集器的優點,很好的解決了拷貝收集策略中,堆記憶體浪費嚴重的問題。

標記整理收集器分為2個階段
1. 標記階段, 這個階段和標記-清除收集器的標記階段相同
2. 整理階段, 這個階段將所有做了標記的活動物件整理到堆的底部(有點像是磁碟碎片整理,呵呵)

生命週期較長的物件,歸入到tenured generation。一般是經過多次minor gc,還 依舊存活的物件,將移入到tenured generation。(當然,在minor gc中如果存活的物件的超過survivor的容量,放不下的物件會直接移入到tenured generation)tenured generation的gc稱為major gc,就是通常說的full gc。由於tenured generaion區域比較大,而且通常物件生命週期都比較長,所以這部分的gc時間比較長。
    minor gc可能引發full gc。當eden+from space的空間大於tenured generation區的剩餘空間時,會引發full gc。這是悲觀演算法,要確保eden+from space的物件如果都存活,必須有足夠的tenured generation空間存放這些物件。

最後補充一點,如果採用的是分代演算法的話還會有一個permanent generation(PSPermGen)區域,該區域比較穩定,主要用於存放classloader資訊,比如類資訊和method資訊。
對於spring hibernate這些需要動態型別支援的框架,這個區域需要足夠的空間。(這部分空間應該存在於方法區而不是heap中)。