Conservative GC (Part two :MostlyCopyingGC )

MostlyCopyingGC

Mostly Copying GC, Joel F.Bartlett, 1989數據結構

此算法能夠在不明確根的環境中運行GC複製算法。函數

概要

Mostly Copying GC就是「把不明確的根指向的對象之外的對象都進行復制」,拋開那些不能移動的對象將其餘大部分的對象進行復制的GC算法。spa

  • 前提
    • 根是不明確的根
    • 沒有不明確的數據結構 :能夠明確判斷對象裏的域是指針仍是非指針
    • 對象大小隨意

堆結構

下圖所示,是 Mostly Copying GC的堆結構,堆被分紅必定大小的頁(page),每一個頁都有編號。那些沒有分配到對象的空頁則有一個$current_space之外的編號。頁編號不能形成數據溢出。3d

GC時,$current_space 和 $next_space用於識別To空間和From空間。編號和$next_space同樣的是To,編號和$current_space同樣的是From頁面。通常狀況下$current_space和$next_space是同一個值,只有在GC時纔會不一樣。$current_space的值會被分配到裝有對象的正在使用的頁。如上圖示,正在使用的頁編號爲1.指針

此外,咱們要爲正在使用的頁設置一下兩種標誌的一種。code

  • OBJECT : 正在使用的頁
  • CONTINUED :當正在使用的頁跨頁時,設置在第二個頁以後。對象

    以上兩個標識不是一塊排列在內存當中,由於會出現跨頁分配對象,因此從實現上來講,咱們必須把頁和標識分配在不一樣的內存位置進行管理。blog

分配

根據分塊的大小、分配對象的大小不一樣,分配的動做也各不相同。內存

  • 若是正在使用的頁裏有符合mutator申請大小的分塊,對象就會被分到這個頁。以下圖示:

  • 當正在使用的頁沒有合適大小的分塊時,對象就會被分配到空的頁,而後正在使用的這個新頁會被設置OBJECT標識。

  • 當mutator申請分配超過頁大小時。分配程序會將對象誇多個頁來分配。和平時同樣,開頭頁設定OBJECT,以後的頁設定CONTINUED。以下圖示:

new_obj()函數

分配的僞代碼

new_obj(size){
    while(size > $free_size) // $free_size用來保持分塊大小
        $free_size = 0 
        add_page(byte_to_page_num(size))
    
    obj = $free
    obj.size = size
    
    if(size < PAGE_SIZE)
        $free_size -= size
        $free += size
    else
        $free_size = 0
        
    return obj
}
  • 將要申請的大小size本身傳給new_obj。
  • 判斷size和$free_size大小。若是$free_size小於size 那麼add_page()函數會分配新的頁,擴大分塊大小。而後新分配的頁數會傳遞給add_pages()函數。
  • $free指向分塊開頭, 將obj設置爲$free
  • 判斷size是否小於$PAGE_SIZE。當size小於頁大小時,則會從$free_size中減去size。也就是當前頁剩餘的大小。而後指針$free向後移動size。
  • 若是size大於$PAGE_SIZE。$free_size變爲0。這樣一來對象就不會被分到CONTINUED頁了。

add_pages()函數

負責從新分頁的add_pages()函數。

add_pages(page_num){
    if($allocated_page_num + page_num >= HEAP_PAGE_NUM/2)
        mostly_copying()
        return 
    
    first_free_page = find_free_pages(page_num)
    
    if(first_free_page == NULL)
        allocation_fail()
    
    if($next_space != $current_space)
        enqueue(first_free_page, $to_space_queue)
    
    allocate_page(first_free_page, page_num)
}
  • $allocated_page_num 表示正在使用的頁數,HEAP_PAGE_NUM表示堆中的總頁數。若是出現「正在使用的頁數+準備追加的頁數>總頁數的一半」,這種狀況下啓動GC mostly_copying()。
  • first_free_pages()函數,在堆內尋找連續的page_num個空頁。若是有則返回最開頭的空頁指針,不然返回NULL表示失敗。
  • 當運行GC複製對象時,爲了使用new_obj() 函數,也會在GC裏調用這個add_ pages() 函數。另外,第三個if的條件只有在 GC 裏才爲真。以後會把GC中分配的頁鏈接上$to_space_queue。當GC執行時,這裏鏈接上$to_space_queue的頁至關於To空間。
  • 最後一行是對於找到連續page_num的指針調用allocate_pages()方法,申請空間。
allocate_pages(first_free_page, page_num){
    $free_page = first_free_page
    $free = first_free_page
    $free_size = page_num*PAGE_SIZE
    $allocated_page_num += page_num
    
    set_space_type(first_free_page, $next_space)
    set_allocate_type(first_free_page, OBJECT)
    
    while(--page_num > 0)
        $free_page = next_page($free_page)
        set_space_type($free_page, $next_space)
        set_allocate_type($free_page, CONTINUED)
    $free_page = next_page($free_page)
    
}
  • 其中set_space_type()函數將新的空頁編號設置成$next_space的值。也就是說,只要在GC裏,這個頁就會當作To空間。set_allocate_type()給函數的頁設置了OBJECT標識。
  • while循環用於分配頁數大於等於2的時候有效。next_space()函數用來返回被用做參數的頁的下一個頁。

GC執行過程

下圖標識GC執行前堆的狀態。這時$current_space和$next_space的值是相同的。首先對$next_space進行增量。一旦GC開始執行,與$current_space值相同的頁就是From頁,與$next_space值相同的頁就是To頁。

以後咱們將那些保留有從根引用的對象的頁「晉升promotion」到To頁。下圖示:這裏的晉升是指將頁的編號設定爲$next_space的值把它當作To空間處理。

由於對象A是根引用的,因此咱們將該對象的頁面編號設定爲$next_space的值。也就是$next_space = 2

把全部從根引用的頁都晉升後,下面就是把To頁裏的對象的子對象複製到空頁了。這個時候對象Y(垃圾對象)引用的D也會被複制過去。而後空頁的編號會被設定爲$next_space。也就是說這個頁變爲了To頁。

接下來,咱們要把追加的To頁裏的對象的子對象複製到To頁的分塊裏。若是To頁裏沒有分塊,那麼對象就會被複制到空頁,目標頁的編號會被設定爲$next_space。上圖中To頁有分塊,因此直接複製對象E。以下圖示:

當全部對象的子對象複製完畢後GC就結束了,此時$current_space的值設定爲$next_space的值。以下圖示:

從上圖得知,垃圾對象X,Y,D都沒有被回收。MostlyCopyingGC的特殊之處就是不會回收包含有從根指向的對象(A)所在頁的垃圾對象,而且也不會回收這個垃圾對象所引用的對象羣。極端一點,若是全部頁裏都有對象被根指着,表明全部垃圾不能被回收。

缺頁能夠經過調整也大小來改善。實驗代表頁大小適合在512字節。實際上本身在生產環境中那個好就是那個了。

mostly_copying()函數

該方法是用來執行GC的函數,由add_pages()調用。

mostly_copying(){
    $free_size = 0 //爲了避免把對象複製到From空間裏去,GC將From頁裏的分塊大小設置爲0
    $allocated_page_num = 0
    
    $next_space = ($current_space) %N // 將next_space進行增量。爲了不$next_space溢出,增量時必取常量N餘數。
    
    for(r :$roots) //保留根直接引用的對象所在頁。
        promote_page(obj_to_page(*r)) //obj_to_page函數將對象做爲參數,返回保留的對的頁。
    
    while(is_empty($to_space_queue) == FALSE) //複製To頁裏的子對象。除去CONTINUED頁,全部的To頁都鏈接到了$to_space_queue。咱們將其取出並傳遞給page_scan().
        page_scan(dequeue($to_space_queue))
    
    $current_space = $next_space
    
}

MostlyCopyingGC不會特地把因GC變空的空頁的編號置爲0.所以空頁的編號可能會很混亂,爲此常量N的數值必須必空頁的總數大得多,以保證及時給全部空頁分配惟一的編號,程序也能識別編號被設爲$next_space的頁和其餘的頁。

promote_page()函數

是將用做參數的頁晉升的函數。若是用做參數的頁 裏的對象跨了多個頁,那麼這些頁都會被一塊兒晉升。

promote_page(page){
    if(is_page_to_heap(page) == True && space_type(page) == $current_space && allocate_type(page)== OBJECT)
        promote_continued_page(next_page(page)) //下面有源碼
        
        // 將晉級的page鏈接到$to_space_queue
        set_space_type(page, $next_space)
        $allocated_page_num++
        enqueue(page, $to_space_queue)
        
}
  • 判斷條件
    • 是否在堆內
    • 頁編號是否和$current_space相同
    • 是否有OBJECT標識
promote_continued_page(page){
    while(space_type(page) == $current_space && allocate_type(page) == CONTINUED) //調查用做參數的頁編號是否爲$current_space,以及是否設置了 CONTINUED 標誌。
        set_space_type(page, $next_space)
        $allocated_page_num++
        page = next_page(page)
}
  • while中調查用做參數的頁編號是否爲$current_space,以及是否設置了 CONTINUED 標誌。若是爲真,則參數的頁裏的對象誇了多個頁,這是所有晉升。

對象不被分配到CONTINUED頁,其緣由就是這裏的最後一行代碼。若是分配到了CONTINUED頁,那麼對象就有可能跨頁,此時CONTINUED頁的下一個頁會頗有可能也是CONTINUED。若是從新放置到一個空頁的話,它是沒有下一頁的。這就形成了本來不用也不想複製的對象因爲在CONTINUED中因此也被複制了

page_scan()函數

把那些持有從根引用的對象的頁所有晉升後,下面就要複製到To頁裏的對象的子對象。

page_scan()函數,是經過mostly_copying()函數調用的函數。這個函數只接受To頁做爲參數。

page_scan(to_page){
    for(obj : objects_in_page(to_page))
        for(child : children(obj))
            *child = copy(*child)
}

這個函數被用於將頁裏全部對象的子對象都交給 copy() 函數,並把對象內的指針都改 寫成目標空間的地址。

copy()函數

將複製對象用做參數。

copy(obj){
    if(space_type(obj_to_page(obj)) == $next_space) //檢查持有obj的頁是不是To頁。若是是就不會被複制直接返回對象。
        return obj
    
    if(obj.field1 != COPIED)  //檢查對象是否複製完畢。
        to = new_obj(obj.size) // 沒有複製完畢,則使用該方法來分配空間
        copy_data(to, obj, obj.size) //將對象複製。
        obj.field1 = COPIED  // 修改複製標記,表示已複製。
        obj.field2 = to // 更改指針地址
    return obj.field2 //返回對象地址(也就是目標空間的地址即原對象forwarding)
}

優缺點

優勢:使用了GC複製算法,包含它的優勢。

缺點:部分垃圾沒有被回收。

黑名單

Hans J.Boehm 黑名單法

保守式GC的缺點之一,就是使用指針識別錯誤,原本要被刪除的垃圾卻被保留了下來,甚至形成其餘更嚴重的錯誤。改善這個問題可採用Hans J.Boehm 發明的黑名單法。

指針的錯誤識別帶來的害處

在指針的錯誤識別中,被錯誤判斷爲活動對象的那些垃圾對象的大小及內容相當重要。

  • 大小:有個巨大的對象死掉了,而保守式 GC 卻把它錯誤識別成「它 還活着」,這樣固然就會壓迫到堆了。
  • 數量:。保守式 GC 會錯誤識別子對象的子對象,以及子對象的子對象的子對象,錯誤就會像多米諾骨牌同樣連續下去。

黑名單

這個黑名單裏記錄 的是「不明確的根內的非指針,其指向的是有可能被分配對象的地址」。咱們將這項記錄操做稱爲「記入黑名單」。 可能被分配的對象的地址指的是堆內未使用的對象的地址

mutator沒法引用至今未使用過的對象若是,根裏存在有這種地址的指針,那它確定就是「非指針」,就會被記入黑名單中。
們在GC標記-清除算法中的 mark() 函數裏導入記入黑名單的操做,其僞代碼以下。

mark(obj){
    if($heap_start <= obj && obj <= $heap_end)
        if(!is_used_object(obj))
            obj.next = $blacklist
            $blacklist = obj
        else
            if(obj.mark == FALSE)
                obj.mark == TRUE
                for(child :children(obj))
                    mark(*child)
}

若是對象正在使用,is_used_obj()就會返回真。在GC開始時候黑名單會被丟棄,也就是說,在標記階段須要注意的地址會被記錄在新的黑名單裏

面向黑名單內存地址分配注意

黑名單裏記錄的是「須要注意的地址」也就是說這個對象就極可能被非指針值所引用。在將對象分配到須要注意的地址時,爲所分配的對象設以下限制條件。

  • 沒有小對象
  • 沒有子對象少,或者說子對象的孩子加起來很少。

優缺點

優勢:保守式 GC 因錯誤識別指針而壓迫堆的問題獲得緩解,堆使用效率提高,沒有多餘對象GC速度也會提高。

缺點:花費時間檢測黑名單。

相關文章
相關標籤/搜索