1. 程式人生 > 其它 >新生代的垃圾回收:Copy GC之基本原理

新生代的垃圾回收:Copy GC之基本原理

據我所能查到的資料,基於複製的GC演算法最早是Marvin Minsky提出來的。

這個演算法的思路很簡單,總的來說,就是把空間分成兩部分,一個叫分配空間(Allocation Space),一個是倖存者空間(Survivor Space)。建立新的物件的時候都是在分配空間裡建立。在GC的時候,把分配空間裡的活動物件複製到Survivor Space,把原來的分配空間全部清空。然後把這兩個空間交換,就是說Allocation Space變成下一輪的Survivor Space,現在的Survivor Space變成Allocation Space。

在有些文獻中,或者實現中,allocation space也會被稱為from space,survivor space也被稱為to space。JVM程式碼中,這兩套命名方式都會出現,所以搞清楚這點比較有好處。我們的文章中,為了方便後面解析JVM程式碼,還是使用from space和to space來指代分配空間和倖存者空間。

copy gc的想法很簡單,但真要使用程式碼實現出來,還是有很多細節要處理的。我從最基本最原始的演算法開始介紹,希望能從淺入深地把這個事情說清楚。

樸素的Copy演算法

最早的,最簡單的copy演算法,是把程式執行的堆分成大小相同的兩半,一半稱為from空間,一半稱為to空間。利用from空間進行分配,當空間不足以分配新的物件的時候,就會觸發GC。GC會把存活的物件全部複製到to空間。當複製完成以後,會把 from 和 to 互換。

用圖來表示就是這樣的:

此時,from空間已經滿了,A物件存活,A又引用了C和D,所以C和D也是活躍的。已經沒有任何地方引用B物件了,那麼B就是垃圾了。這時候,如果想再建立一個新的物件就不行了,這時就會執行GC演算法,將A,C,D都拷貝到新的空間中去。

然後把原來的空間全部清空。這樣,就完成了一次垃圾回收。

有幾個問題要解決,第一個問題,如何判斷A和B是否存活。因為C和D是被A引用的,那麼,A如果是存活的,C和D就是存活的,這個相對簡單一些。我們先看一下Java程式中怎麼樣的:

public static void foo() {
    A a = new A();
    bar();
    E e = new E();
}

public static void bar() {
    B b = new B();
}

在上面的例子中,在建立物件E的時候,已經從bar的呼叫中返回了。這個時候,物件a還存活於foo的呼叫棧裡,而b已經沒有任何地方會去引用它了——原來唯一的引用,bar的棧空間,已經消失了。所以b就變成了垃圾。而這個時候由於from空間不足,無法正確地建立E,所以,就會執行GC,這時候 b 做為垃圾就被回收了。

可見,如果存在一個從棧上出發到物件的引用,那麼這個物件就是存活的。所以我們把棧上的這種引用稱為roots。roots包含很多內容,除了棧上的引用,JNIHandle,Universe等等都會有向堆上的引用,但在我的文章裡,只以棧上的引用做為roots來講解。

Copy的實現

複製GC演算法,最核心的就是如何實現複製。我用虛擬碼來表示:

void copy_gc() {
    for (obj in roots) {
        *obj = copy(obj);
    }
}

obj * copy(obj) {
    if (!obj.visited) {
        new_obj = to_space.allocate(obj.size);
        copy_data(new_obj, obj, size);
        obj.visited = true;
        obj.forwarding = new_obj;

        for (child in obj) {
            *child = copy(child);
        }
    }

    return obj.forwarding;
}

演算法的開始是從roots的遍歷開始,然後對每一個roots中的物件都執行copy方法。

如果這個物件沒有被訪問過,那麼就在to space中分配一個與該物件大小相同的一塊記憶體,然後把這個物件的所有資料都拷貝過去(copy data),然後把它的visited標記為true,它的forwarding記為新的地址。

接著遍歷這個物件的所有引用,執行copy。這個過程是一個典型的深度優先搜尋。

然後,還有最重要的一步,把forwarding做為返回值,返回給用者,讓它更新引用。為什麼要有forwarding這個屬性呢?看起來很麻煩的樣子,我直接在分配完了就把引用我的指標改掉不就行了嗎?

考慮這樣一種情況,A和B都引用了C:

然後我們執行copy演算法,A先拷到to space,然後C又拷過去,這時候,空間裡的引用是這種狀態:

A和C都拷到新的物件裡了,原來的引用關係還是正確的,美滋滋。但是B就慘了,它並不知道自己引用的C已經被拷貝了。當我們訪問完B以後,對於它所引用的C,只看到C已經被搬走了,但並不知道搬到哪裡去了。這就是forwarding的含義,我們可以在C上留下一個地址,告訴後來的人,這個地址已經失效了,你要找的物件已經搬到某某地了,然後你只要把引用更新到新的地址就好了。

舉個例子,你有一個通訊錄,上面記了你朋友家的地址在東北旺西路南口18號,當你按這個地址去找他的時候,看見他家門口貼了一張紙條,說我已經搬到北京西站南廣場東81號了。好,那你就可以把這個新的地址更新到通訊錄裡了,下次,你按照新的地址還能找到你的朋友。

如上圖所示,當B再去訪問C的時候,就看到C已經被拷貝過了,而C通過forwarding指標引用了新的地址,那麼B就可以根據這個新的地址把自己對C的引用變成對C'的引用。

Copy GC的特點

在普通的應用程式中,有大量的物件生命週期並不長,很多都是建立了以後沒多久,就會變成垃圾(例如,一個函式通出以後,它所使用的區域性變數都全都是垃圾了)。所以,在執行Copy GC時,存活的物件會比較少。我們執行Copy GC只要把還存活的物件拷貝到倖存者空間就可以了。當存活物件少的時候,GC演算法的效率就會比較高。這時,演算法就有很高的吞吐量。

至於Copy GC的更多內容,我們下期再講。