新生代的垃圾回收: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的更多內容,我們下期再講。