GC:標記清除算法

本文主要介紹標準的標記-清除算法的過程,優缺點,以及做的一些優化過程。

1.GC標記清除算法

標記清除算法主要分爲兩個階段:標記、清除。

標記階段:將所有的活動對象做上標記;
清除階段:把內存裏面沒有打上標記的對象回收掉。

通過這兩個階段,就可以把不需要的空間重複利用。

下面詳細介紹一下標記階段和清除階段是怎麼做的。

假設現在執行GC前有一塊分塊堆狀態如下圖:
在這裏插入圖片描述

1.1 標記階段

標記階段的僞代碼如下:

mark_phase(){
	for(r : $roots) {
		 mark(*r)
	}
}

collector從遍歷所有的根節點出發,標記所有的活動對象。然後通過調用遞歸函數mark()函數,mark函數僞代碼如下:

mark(obj){
	if(obj.mark == FALSE){ //對象沒有被標記
		//設置對象是活動對象
		obj.mark = TRUE
		for(child : children(obj)){ // 遞歸對象的子節點
			mark(*child)
		}
	}

第二行判斷 if(obj.mark == FALSE) 主要是針對循環引用的場景,避免循環引用時候會循環調用mark函數,避免重複進行標記處理。

這個標記obj.mark 肯定是需要保存到對象的頭裏面。

標記完所有活動對象後,標記階段就結束了。標記階段結束後堆的狀態如下圖:
在這裏插入圖片描述

由上面標記階段可知,標記階段會遍歷堆中所有對象,所以標記階段耗時是與“活動對象的總數”成正比的。

1.2 標記階段算法

前面已經說了,標記階段需要遍歷所有的對象,那麼怎麼遍歷呢?常見的是基於深度優先遍歷搜索或則是廣度優先遍歷搜索。

針對這兩種場景來說,一般深度優先遍歷會比廣度優先遍歷搜索使用的內存量會更少,所以一般通過深度優先去做搜索。

1.3 清除階段算法

清除階段,collector也會遍歷整個堆,然後回收釋放所有沒有打標的內存對象。

僞代碼如下:

sweep_phase(){
	sweeping = $heap_start //起始指針
	while(sweeping < $heap_end){
		if(sweeping.mark == TRUE){
			 sweeping.mark = FALSE
		} else{
			sweeping.next = $free_list 
			$free_list = sweeping
		}
		sweeping += sweeping.size }
	}
}

在此出現了叫作 size 的域,這是存儲對象大小(字節數)的域。跟 mark 域一樣,我們事先在各對象的頭中定義它們。

清除階段,通過變量 sweeping 遍歷堆,具體來說就是從堆首地址 $heap_start 開始,按順序一個個遍歷對象的標誌位。

遍歷回收過程中,我們用free_list空閒鏈表來鏈接所有被回收的空閒內存塊。

經過清除階段之後內存堆的狀態如下圖:
在這裏插入圖片描述

清除階段,程序會遍歷所有堆,進行垃圾回收所花費時間與堆大小成正比。堆越大,清除階段所花費的時間就會越長。

1.4 已回收空閒內存空間再分配

當我們程序需要申請新的內存空間時,GC怎麼做分配才能利用空閒列表呢?

前面我們在清除階段已經把垃圾對象連接到空閒鏈表了。搜索空閒鏈表並尋找 大小合適的分塊,這項操作就叫作分配。執行分配的函數 new_obj() 僞代碼如下:

new_obj(size){
	chunk = pickup_chunk(size, $free_list) 
	if(chunk != NULL)
		return chunk
	else
		allocation_fail()
}

上面的邏輯很簡單,主要依賴於pickup_chunk函數從空閒鏈表取出合適的內存空間。

pickup_chunk() 函數用於遍歷 $free_list,尋找大於等於 size 的分塊。它 不光會返回和 size 大小相同的分塊,還會返回比 size 大的分塊。如果它找到和 size 大小 相同的分塊,則會直接返回該分塊;如果它找到比 size 大的分塊,則會將其分割成 size 大 小的分塊和去掉 size 後剩餘大小的分塊,並把剩餘的分塊返回空閒鏈表。

如果此函數沒有找到合適的分塊,則會返回 NULL。返回 NULL 時分配是不會進行的。針對這種情況,有專門的allocation_fail() 函數處理。

這裏從free_list搜索空閒合適內存時候,會有一些策略的不同優化。主要有First -fit、Best -fit、Worst -fit這些策略,具體的過程也比較簡單,感興趣的可以自行Google。

1.5 合併(內存碎片整理)

經過一定次數的分配之後,肯定會產生很多的內存碎片。但如果它們是連續的, 我們就能把所有的小分塊連在一起形成一個大分塊。這種“連接連續分塊”的操作就叫作合 並(coalescing),合併是在清除階段進行的。具體的過程這裏就不說明了。

2.GC標記清除算法的優缺點

優點:
1)首先就是簡單,整個算法過程看上去還是比較easy的。

缺點:
1)很明顯,這種方式會造成大量的內存碎片,對碎片化的合併也會大大增加GC的負擔。
2)分配速度:遍歷空閒列表來分配導致的效率肯定是不高的。

3. GC標記清除算法的優化

針對標記-清楚算法的缺點也會有一些優化。

3.1 multi-size空閒鏈表優化分配速度

前面講了分配操作時候需要遍歷空閒鏈表直到找到合適的內存大小,時間複雜度很明顯的O(n)。

我們有一種方法,就是利用分塊大小不同的空閒鏈表,即創建只連接大分塊的空閒鏈表和只連接小分塊的空閒鏈表。這樣一來,只要按照 程序 所申請的分塊大小選擇空閒鏈表,就能在短時間內找到符合條件的分塊了。

我們用一個空閒鏈表數組保存不同大小的空閒鏈表首地址。主要策略是:

1)針對小字節的碎片,一個size一個空閒列表;
2)針對大size的塊,單獨一個列表。

3.2 延遲清理

我們知道清理階段耗費的時間與堆的大小有關係,爲了降低STW的時間,標記階段之後並不做及時的清理,而是在分配時候做檢測然後清理,這樣就可以減少STW的時間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章