目錄算法
GC 標記-清除算法是由標記階段和清除階段構成。標記階段把全部活動的對象作上標記。清除階段是吧沒有標記的對象也就是非活動的對象進行回收。經過這兩個階段就能夠對空間進行從新利用。windows
mark_sweep()函數數組
mark_sweep(){ mark_phase() //標記 sweep_phase() //清除 }
在執行GC前堆的狀態
函數
使用 mark_phase()函數進行處理。在標記階段,collector會爲堆裏全部活動的對象打上標記。咱們首先要經過根找到直接引用的對象進行標記,以後才能遞歸的訪問到對象並把全部活着的對象進行標記。性能
mark_phase()操作系統
mark_phase(){ for(r:$roots) mark(*r) //從根開始取它的孩子進行標記 } mark(obj){ if(ob.mark == FALSE) obj.mark = TRUE // 若是仍是標記是FALSE則改成TRUE for(child :children(obj)) // 再去遞歸的標記本身的孩子 mark(*child) }
標記完全部對象後,標記活動就結束了。這時,狀態以下圖所示。3d
在標記階段中,程序會標記全部活動對象,毫無疑問,對象越多須要的時間就越多。他們是正相關的。unix
這時一個老生常談的問題了。應用很廣,由於像這種類型可能沒有其餘方式遍歷了,其使用的隊列和遞歸也是很是經常使用的。code
深度優先depth-first search 使用遞歸實現對象
廣度優先breadth-first search 使用隊列實現
不管使用哪一種方法,搜索的對象時不會變的。可是深度優先能夠壓低內存的使用量,所以咱們常常實用深度優先。
在清除階段,cllector會遍歷整個堆。回收垃圾,使其能再次利用。此時出現了size的域這是存儲對象大小的域,同mark同樣咱們在頭中定義了它。簡單的說,在塊的頭中如今有mark和size。mark表示時候有用,size表示塊的大小。
sweep_phase()
sweep_phase(){ sweeping = $heap_start //堆頭 while(sweeping < $heap_end) //如今爲止在堆尾以前 if(sweeping.mark == TRUE) SWEEPING.mark = FALSE //這裏標記爲false。由於若是之後再也不使用了她就能被回收,不然就不能回收了。 else sweeping.next = $free_list //初始化空鏈表 $free_list = sweeping // 鏈接到一個free鏈表上 sweeping += sweeping.size //能夠看作是基址+偏移。拿到下一個塊地址 }
經過上面的操做,咱們拿到了free_list。以後咱們針對它進行分配空間操做。下圖就表現出gc後兩個鏈表
清除階段同理,會遍歷全部塊進行垃圾回收。時間和堆的大小成正相關。
那麼對於垃圾回收的再利用是怎麼進行的呢?。當mutator申請分塊時,怎樣才能分呢?
咱們使用nwe_obj函數來分配。
new_obj
nwe_obj(){ chunk = pickup_chunk(size, $free_list) // 我須要的大小size 和查找對象 free_list if (chunk !=NULL) return chunk //拿到了 else allocation_fail() // 拿不到 }
在分配的時候有幾個點:不能分配比size小的,只能使用大於等於size的塊去進行分配,若是大小合適再好不過,若是大了就須要去分割一個和size同樣大小的塊再分配,剩餘部分返回空鏈表。
綜上, 考慮到碎片和性能問題,選擇First-fit仍是比較明智的選擇。
根據分配策略會生成大量小塊,可是若是小塊是連續的咱們其實能夠將它鏈接起來成爲一個大塊。這就叫作合併coalescing
sweep_phase()
sweep_phase(){ sweeping = $heap_start //堆頭 while(sweeping < $heap_end) //如今爲止在堆尾以前 if(sweeping.mark == TRUE) SWEEPING.mark = FALSE //這裏標記爲false。由於若是之後再也不使用了她就能被回收,不然就不能回收了。 else if(sweeping = $free_list+$free_list.size)// 先驗證是否是相鄰的。 $free_list.size +=sweeping.size //這樣就鏈接起來了。 else sweeping.next = $free_list //初始化空鏈表 $free_list = sweeping // 鏈接到一個free鏈表上 sweeping += sweeping.size //能夠看作是基址+偏移。拿到下一個塊地址 }
他其實就是用到了標識位,而後進行遍歷。算法上難度很低。不像其餘算法難度是比較大的。
在保守式GC中,對象時不能被移動的。例如複製算法他就是將數據複製過來,在複製過去,是移動的。而標記清除不移動對象,因此很是適合保守式GC算法。實際上不少採用保守式gc的程序中使用到了標記清除。
算法使用過程當中產生被細化的分塊,在不久後就會散落在各處。咱們將這種情況稱爲碎片化(fragmentation)。windows文件系統也會有這種現象。
若是發生了碎片化,及時空閒鏈表再大,也不能分配成功。爲了解決這個問題能夠採用壓縮,可是本文中介紹了一下BIBOP法提供參考。
標記清除算法中分塊不是連續的,所以每次都須要遍歷鏈表。在最糟糕的狀況下是每次是鏈表的最後一塊。所以速度很是慢,若是一個大型遊戲採用這種方式後果可想而知。
後文敘述的多個空閒鏈表和BIBOP都是爲了解決速度而採起的方案。
寫時複製技術(copy-on-write)是在Linux等衆多unix操做系統的虛擬存儲中使用的高速化方案。例如在Linux複製進程使用fork()時,大部分空間都不會被複制。若是說爲了複製進程就複製了全部空間這樣內存怎麼多也不夠。所以寫時複製技術就僞裝複製了內存空間其實是共享內存空間。
固然咱們對共享的空間寫入時不能直接寫它,由於它是別的。這時候就要將其複製到本身的私有空間而後進行重寫。複製後之訪問私有空間不訪問共享空間。
可是標記清除算法,就算是沒有重寫,也會進行不斷的複製。實際上咱們仍是但願它是用共享,而不是浪費內存。爲了處理這個問題咱們採用爲圖標記法bitmap marking。
以前的算法中使用的只有一個空閒鏈表,對大塊和小塊進行統一的處理。這樣一來不管大小都要遍歷很長的鏈。
所以咱們對塊進行分類。大的是一組,小的是一組。這樣一來按照mutator所申請的空間大小選擇合適的塊就容易的多。
通常狀況來講,mutator不多會申請很是大的分塊。爲了應對這種極少數狀況,咱們給分塊設定一個上限。若是分塊大於等於這個大小,就所有采用一個空鏈表。
利用多個空鏈表時,咱們須要修正new_obj()以及sweep_phase()
new_obj
nwe_obj(){ index = size/(WORD_LENGTH/BYTE_LENGTH) //根據商來選擇合適的建表 if (index <=100 ) //小於100字 if($free_list[index] !=NULL) chunk = $free_list[index] // 獲取鏈的索引,分給他一個index字鏈上的第一塊 $free_list[index] = $free_list[index].next return chunk else //大於100字 chunk = pickup_chunk(size, $free_list[101]) // 我須要的大小size 和 查找對象 free_list if (chunk !=NULL) return chunk //拿到了 else allocation_fail() // 拿不到 }
sweep_phase()
sweep_phase(){ for(i:2...101) $free_list[1] = NULL sweeping = $heap_start //堆頭 while(sweeping < $heap_end) //如今爲止在堆尾以前 if(sweeping.mark == TRUE) SWEEPING.mark = FALSE //這裏標記爲false。由於若是之後再也不使用了她就能被回收,不然就不能回收了。 else index = size/(WORD_LENGTH/BYTE_LENGTH) if(index <=100) sweeping.next = $free_list[index] $free_list[index] = sweeping else sweeping.next = $gree_list[101] $free_list[101] = sweeping sweeping += sweeping.size //能夠看作是基址+偏移。拿到下一個塊地址 }
BiBOP是Big Bag Of Pages的縮寫,就是將大小相近的對象整理成固定大小的塊進行管理的作法。咱們可使用這個方法把堆分紅固定大小的塊,讓每一個塊只能配置一樣大小的對象這就是BiBOP算法。不以爲內存利用效率低嗎?熊die。
如圖所示,在多個分塊中殘留一樣大小的對象反而會使堆的使用效率低下。
在之前的標記清除算法中,標記是在對象頭中的,這樣形成了與寫時複製技術的不兼容。對此收集頭部標識表格化將標識與對象分開管理。這樣的標記方法稱爲位圖表格(bitmap table)。位圖標記能夠採用如散列表,樹形結構等。爲了簡單起見咱們使用數組。以下圖。
表中的位置和堆裏的對象一一對應。通常來講堆中的一個字會分到一個位。
mark()
mark(obj){ obj_num = (obj-$heap_start)/WORD_LENGTH index = obj_num / WORD_LENGTH offset = obj_num % WORD_LENGTH if (($bitmap_tbl[index]&(1<<offset))==0) $bitmap_tbl[index] != (1<<offset) for (child :children(obj)) mark(*child) }
這裏WORD_LENGTH 是個常量,表示各機器中1個字的位寬。obj_num指從位圖表格前面數起,obj的標誌位在第幾個。例如上圖中E,它的obj_num就是8.可是bitmap的圖是從後往前的因此E的標誌位應該從右往左數是第九個位。以下圖
以往標記位是對對象進行設置,而位圖標記不對對象進行設置採用了映射或許能夠理解爲引用。因此天然不會發生無謂的複製。
利用位圖表格的清除操做把全部對象的標誌位集合到一處,能夠快速定位。與通常的清除階段相同,咱們sweeping遍歷整個堆,不過這裏使用了index和offset兩個變量,在遍歷堆的同時也遍歷位圖表格。
sweep_phase
sweep_phase(){ sweeping = $heap_start index = 0 offset = 0 while(sweeping <$heap_end) if($bitmap_tb1[index] &(1<<offset) ==0) sweeping.next = $free_list $free_list = sweeping index +=(offset + sweeping.size) /WORD_LENGTH offset = (offset + sweeping.size) % WORD_LENGTH sweeping += sweeping.size for (i:0...(HEAP_SIZE/WORD_LENGTH-1)) $bitmap_tbl[i] = 0 }
對象地址和位圖表格對應。經過對象的地址求與其對應的位置標誌,要進行位運算的。再有多個堆且地址不連續的狀況下,必須採用多個表。即每一個堆一個表。
咱們以前說過,清除花費的時間和堆的大小成正比。這樣一來堆越大就越影響mutator。會越慢。
延遲清除(Lazy Sweep)是縮減因清除操做而致使的mutator最大暫停時間的方法。在標記操做結束後,不一併進行清除。經過延遲來防止mutator長時間暫停。
nwe_obj(size){ chunk = lazy_sweep(size) if (chunk!=NULL) return chunk mark_phase() chunk = lazy_sweep(size) if(chunk !=NULL) return chunk allocation_fail() }
分配時調用lazy_sweep進行清除。若是他能用清除操做來分配塊,就會返回分開,若是不能分配塊就會執行標記操做。當lazy_sweep函數返回NULL時就是沒有找到。那就在進行一遍此操做。若是還沒能分得表明沒有分塊。mutator也就不須要進行下一步處理。
lazy_sweep(size){ while($sweeping <heap_end) if($sweeping.mark == TRUE) $sweeping.mark = FALSE else if($sweeping.size >=size) chunk = $sweeping $sweeping +=$sweeping.size return chunk $sweeping += $sweeping.size $sweeping = $heap_start return NULL }
次函數會一直遍歷堆,知道找打大於等於所申請的空間。再找到時候會將其返回,可是$sweeping是全局變量,也就是說遍歷開始位置謂語上一次清除操做中發現的分塊右邊。
當次函數沒有找打分塊時候會返回NULL。
此方法在分配時執行必要的遍歷,所以能夠壓縮清除操做致使mutator暫停的時間,這就是延遲的意思。
雖然能夠減小mutator的暫停時間。可是延遲清除的效果是不均勻的。打個比方以下圖。
垃圾變成了垃圾堆,活動對象變成了活動對象堆。這種狀況下,程序清除垃圾較多的部分時立刻就能得到分塊,隨意能減小mutator的暫停時間。然鵝一旦程序開始清除活動對象周圍就怎麼也得到不了分塊,這就增長了mutator的暫停時間。
至於有什麼其餘方法。後續會在其餘文章裏。