Mark Sweep GC

標記清除算法

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、Best-fit、Worst-fit三種分配策略

  • First-fit:最初發現大於等於size的分塊就進行操做。可能第一個就比size大,可是第二個就和size同樣大。這樣就直接分了第一個,碎片嚴重。
  • Best-fit:遍歷空鏈表,選擇最合適的。性能低啊。
  • Worst-fit:找出空閒鏈表中最大的塊,分割成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算法。實際上不少採用保守式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法

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

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

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的暫停時間。

至於有什麼其餘方法。後續會在其餘文章裏。

相關文章
相關標籤/搜索