1. 程式人生 > >Copying GC (Part two :Multi Space Copying GC)

Copying GC (Part two :Multi Space Copying GC)

目錄

近似深度優先搜尋方法

Paul R.Wilson、Michael S.Lam、Thomas G.Moher,1991

這個方法只是近似深度優先搜尋,但可以做到深度優先執行GC複製演算法。

Cheney的GC複製演算法

假設所有物件都是2個字,下圖所示是物件間的引用關係。

下圖所示是執行該演算法時候,各個物件所在的頁面

(頁面,在作業系統,和計算機組成原理課程中都有)。

右上角數字是頁面編號,假如說頁面容量是6個字(只能放3個物件)。

從上圖不難看出,A,B,C是相鄰的,這就是比較理想的狀態。對於其他物件來說,降低了連續讀取的可能性,降低了快取命中率。

在下面1-4頁中,同一個頁面的物件甚至都沒有引用關係(頁面1中D和頁面2中HI,有引用關係,但是不命中,需要讀記憶體資料到catch),這樣就不得不從記憶體上再去讀。一直這樣下去可想而知,有很多的物件會是這樣的分佈狀態。

前提

在這個方法中有下面四個變數。

  • $page: 將堆分割成一個個頁面的陣列。$page[i]指向第i個頁面的開頭。
  • $local_scan:將每個頁面中搜索用的指標作為元素的陣列。$local_scan[i]指向第i個頁面中下一個應該搜尋的位置。
  • $major_scan:指向搜尋尚未完成的頁面開頭的指標。
  • $free:指向分塊開頭的指標。

先複製A到To空間,然後複製他們的孩子B,C,都被放置到了0頁。如下圖示:

  • 因為A已經搜尋完畢,所以$local_scan[0]指向B。
  • $free指向第一頁的開頭,也就是說下一次複製物件會被安排在新的頁面。在這種情況下,程式會從$major_scan引用的頁面和$local_scan開始搜尋。
  • 當物件被複制到新頁面時,程式會根據這個頁面的$local_scan進行搜尋,直到新頁面物件被完全佔滿為止。
  • 此時因為$major_scan還指向第0頁,所以還是從$local_scan[0]開始搜尋,也就是說要搜尋B。

  • 複製了D(B引用的物件),放到了$page[1]開頭。像這樣的頁面放在開頭時候,程式會使用該頁面的$local_scan進行搜尋。此時$local_scan[0]暫停,$local_scan[1]開始。之後複製了H,I。

  • 這裡第一頁滿了,所以$free指向第二頁開頭。因此$local_scan[1]暫停搜尋,程式$local_scan[0]開始搜尋。(即對B物件再次進行搜尋,看有沒有其他孩子。)

  • 可以看到B的孩子E被複制到了$page[2],同樣,對$local_scan[0]再次進行暫停,對E用local_scan[2]進行搜尋。
  • 因此複製了J,K。

  • 通過對J,K的搜尋頁面2滿了,$free指向了頁面3。再次回到$local_scan[0]進行搜尋。
  • 搜尋完物件C,複製完A到O的所有物件之後狀態如下圖所示。

這樣就搜尋完了第0頁($major_scan),雖然還沒有搜尋完子物件,但是孩子沒有孩子,所以現在這個狀態,和搜尋完後是一樣的。

執行結果

該方法是如何安排物件的呢?如下圖示:

很明顯能看出與Cheney的複製演算法不同,不管下一個頁面在哪裡,物件之間都存在引用關係。

該方法,採用了不完整的廣度優先,它實際上是用到了暫停的。從一開始我們就根據關係,然後進行暫停,將有關係的物件安排到了一個頁面中。

多空間複製演算法

GC複製演算法最大的缺點就是隻能利用半個堆。

但是如果我們把空間分成十份,To空間只佔一份那麼這個負擔就站到了整體的1/10。剩下的8份是空的,在這裡執行GC標記清除演算法。

多空間複製演算法,實際上就是把空間分成N份,對其中兩份進行GC複製演算法,對其中(N-2)份進行GC標記-清除

multi_space_copying()函式

muti_space_copying(){
    $free = $heap[$to_space_index]
    for(r :$roots)
        *r = mark_or_copy(*r)
        
    for(index :0..(N-1))
        if(is_copying_index(index) == FALSE)
            sweep_block(index)
            
    $to_space_index = $from_space_index
    $from_space_index = ($from_space_index +1) % N
}

將堆分為N等份,分別是$heap[0],$heap[1]...$heap[N-1]。這裡的$heap[$to_space_index]表示To空間,每次執行GC時,To空間都會像$heap[0],$heap[1]...$heap[N-1],$heap[0],這樣進行替換。Form空間在To空間的右邊,也就是$heap[1]...$heap[N-1]。

  • 其中第一個for迴圈,為活動物件打上標記。能看出來是標記清除演算法中的一個階段。
  • 其中第一個for迴圈,當物件在From空間時,mark_or_copy()函式會將其複製到To空間,返回複製完畢的物件。如果obj在除Form空間以外的其他地方mark_or_copy()會給其打上標記,遞迴標記或複製它的子物件。
  • 其中第二個for迴圈,是清除階段。對除From和To空間外的其他空間,把沒有標記的物件連線到空閒連結串列。
  • 最後將To和From空間向右以一個位置,GC就結束了。

mark_or_copy()

mark_or_copy(obj){
    if(is_pointer_to_from_space(obj) == True)
        return copy(obj)
    else
        if(obj.mark == FALSE)
            obj.mark == TRUE
            for(child :children(obj))
                *child = mark_or_copy(*child)
        return obj

}

調查引數obj是否在From空間裡。如果在From空間裡,那麼它就是GC複製演算法的物件。這時就通過copy()函式複製obj,返回新空間的地址。

如果obj不在From空間裡,它就是GC標記-清除演算法的物件。這時要設定標誌位,對其子物件遞迴呼叫mark_or_copy()函式。最後不要忘了返回obj。

copy()

copy(obj){
    if(obj.tag != COPIED)
        copy_data($free, obj, obj.size)
        obj.tag = COPIED
        obj.forwarding = $free
        $free += obj.size
        for(child :children(obj.forwarding))
            *child = mark_or_copy(*child)
        return obj.forwarding
}

遞迴呼叫不是copy()函式,而是呼叫mark_ or_copy()函式。如果物件*child是複製物件,則通過mark_or_copy() 函式再次呼叫這個copy()函式。

執行過程

將記憶體分為4等份。如下圖示:

To空間$heap[0]空著,其他三個都被佔用。這個狀態下,GC就會變為如下如示:

我們將$heap[0]作為To空間,將$heap[1]作為From空間執行GC複製演算法。此外$heap[2]和$heap[3]中執行GC標記-清除演算法,將分塊連線到空閒連結串列。

當mutator申請分塊時候,程式會從空閒連結串列或者$heap[0]中分割出塊給mutator。

接下來,To空間和From空間都向後移動一個位置。mutator重新開始。

這次$heap[1]是To空間,$heap[2]From空。這種狀態下執行就會變為下圖所示:

$heap[2]的活動物件都被複制到了$heap[1]中,在$heap[0]和$heap[3]中執行GC標記清除。然後From和To後移一次。

優缺點

優點

提高記憶體利用率:沒有將記憶體空間二等分,而是分割了更多空間。

缺點

GC標記清除,分配耗時,分塊碎片化。當GC標記清除演算法的空間越小的時候,該問題表現的越不突出。例如將記憶體分為3份的情況下。