內存回收的一些基本方法

內存垃圾回收(Garbage Collection)是一個很古老的技術了,最開始在Lisp上出現。如今幾乎所有高級語言都有GC,大部分程序員不再需要絞盡腦汁通宵達旦去查找內存泄露的原因了。我以前也不怎麼關心垃圾回收這個問題,可是面試時老是被問到智能指針,而我又不會寫,因爲我對C++不熟。所以決定研究並且總結一下這個問題。
其實智能指針都不能稱爲GC,就是編譯器給你加了delete或free,基於的原理是引用計數(Reference Counting)。GC一般基於一下兩個原理

Reference Counting(引用計數): 每個對象都設置一個參數,就是引用它的變量,引用少一個就減1,多一個就加1,爲0時回收
Reachability(可達性):有一組基本的對象或變量是可達的,稱爲root set,這些變量或對象指向的對象也是可達的,同理,一個可達對象指向的對象是可達的。

本文簡單的介紹了常用的幾種內存回收算法,包括Reference Counting,Mark and Sweep,Semispace, Generation。

Reference Counting

一般沒有真正的GC使用Reference Counting。智能指針使用了Reference Counting,在指針析構的時候,將引用數減1,爲0時順便把指向的對象回收了。

一個簡單的智能指針的實現(用於應付面試)

template <class T> class SmartPointer {
    protected:
    T* ref;
    unsigned int * ref_count;
    public:
    SmartPointer(T *ptr)
    {
        ref = ptr;
        ref_count = (unsigned int*)malloc(sizeof(unsigned int));
        *ref_count = 1;
    }
    SmartPointer(SmartPointer<T> & sptr)
    {
        ref = sptr.ref;
        ref_count = sptr.ref_count;
        ++*ref_count;
    }
    SmartPointer<T> & operator= (SmartPointer<T> &sptr)
    {
        if(this != &sptr)
        {
            ref = sptr.ref;
            ref_count = sptr.ref_count;
            ++*ref_count;
        }
        return *this;
    }
    ~SmartPointer()
    {
        --*ref_count;
        if(*ref_count == 0)
        {
            delete ref;
            free(ref_count);
            ref = NULL;
            ref_count = NULL;
        }
    }
    T getValue() {return *ref;}
}

智能指針是最簡單的一種gc方法。甚至,這算不上一種gc,實際上是編譯器幫你寫了free或者delete,基於的原理就是:對象的作用域結束時都會自動調用析構函數,這個析構函數是編譯器在編譯時加上的。gc都會有一個觸發事件,對於智能指針來說,就是作用域結束。對於其他的,可能是內存不夠了,然後會啓動gc進行回收。

Mark and Sweep

Mark and Sweep使用的是可達性。在一個程序中,所有的全局變量,靜態變量,局部變量都是可達的,這些稱爲root set。從root出發,找到所有可達的,然後回收不可達的。
基本的過程如下:
每個object都有一個singlebit的標誌位,一開始都是0
要回收的時候,掃兩遍
第一遍,從root變量開始進行DFS掃描,可達的都將它們的標誌位置1
第二遍,搜索所有的object,如果是1,置爲0,如果是0,reclaim

這就有一個問題,這個root怎麼找呢?比如C語言,怎麼確定找出棧上哪些是變量?更不用說要確定哪些是指針了。對於高級的動態語言,虛擬機或者解釋器都會維護一個所有符號的表,這樣找起來是很容易的。gc可以分爲Precise gc和Conservative gc。前者明確知道內存的哪個地方是變量,哪個地方是指針,因此可以精確的進行回收,這種一般適用於高級語言,例如lisp,python,Java等。但是對於C語言,只能假設棧上任何32bit(或者64bit)都是指針,在此基礎上可能會有一些檢測方法,然後把這些指針當作root,進行掃描。C/C++還有一個問題就是internal pointer。因爲在高級語言裏,一般所有的地址都指向對象的開頭,但是C/C++指針可以指向對象的任何地方,這也導致了掃描的困難。所以C/C++一般不會使用gc。This is the nature of C! 但是也有一些比較好的C/C++的gc,例如Boehm GC,它是一種Conservative GC。

Boehm挺好用的,下面是一個例子。

#include <stdio.h>
#include <gc/gc.h>
int main()
{
    int i;
    GC_INIT();
    int *p;
    for(i = 0; i < 1000000; i++)
    {
        p = (int*)GC_MALLOC(20*1024*1024);
        p[i/400] = 5;
        if(i % 10000 == 0)
            printf("Heap size = %d\n",GC_get_heap_size());
    }
}

這段代碼不會發生內存溢出,如果使用malloc但是不free,很快內存就不夠了。
但是如果我把大小從20*1024*1024增加到1024*1024*1024,就有問題了。

內存不夠用了。說明它的回收做的不夠好。而使用malloc加free,可以一直運行下去。我的內存有2G,是夠用的。Boehm GC是最有名的C/C++ GC,而且不少項目也在用它。但是,C語言的本性決定了它不需要GC。

Semispace

在進行內存回收時,內存整理也是必須的。否則內存中充滿了碎片。Semispace的方法也是基於可達性,從名字也可以看出,它是要把內存分成兩半,只有一半可用,一個FromSpace,一個ToSpace(或者叫Old,New,whatever)。

基本工作過程是:
從root開始掃描,找到可達的,就從FromSpace複製到ToSpace,一直這樣找下去,最後可達的都被移到了ToSpace,而且不存在碎片。
這個過程牽涉到一個很嚴重的問題:指針重定向,稱爲pointer forward。這是semispace需要解決的最主要問題。這個問題最簡單的方法就是查表。

copy(p):
    if(content of p is already copy to ToSpace)
         p = forwarding_address(p)
         ret
    if(content of p is not copied to ToSpace)
         copy content of p to ToSpace
         forwarding_address(p) =  ToSpacePtr;
         ToSpacePtr += sizeof(p)
    foreach pointer x in content of p:
          copy(x)

如果回收的時候堆裏大部分都是garbage,那麼semispace的方法特別好,如果大部分都是可達的,那麼效率就很低了。

Generation Garbage Collection

如果你在程序裏讀入了一些靜態的數據,很大,而且需要常駐內存,而且裏面確定沒有指針。你肯定不希望GC一直去掃描它或者一直移來移去。Java和.Net採用的方法稱爲Generation Garbage Collection,將對象分成幾個generation,新創建的對象在 Generation 0(Java使用Young,Old,Permenant,Eden,Survior,Tenured,.Net使用0,1,2),逃過第一次掃蕩(Sweep)的被挪到Generation 1,逃過兩次的被挪到Generation 2,.Net就到2,就是你逃過回收的次數越多,就越年老,GC就越不管你。
基本的過程如下:

if(G0 is almost full)
{
     scan and reclaim G0
     if(G1 is almost full)
     {
          scan and reclaim G1
          if(G2 is almost full)
               scan and reclaim G2
          move survivors to G2
     }
     move survivors to G1
}

這張圖是Java使用的方法,先分了Young,Old,Permanent,然後裏面又細分,挺複雜的,但是思想就是上面所敘述的。

總結

本文只是簡單的介紹了垃圾回收的一些基本思想方法,實際上GC特別複雜。自動回收的代價就是性能的下降,在有些情況下自動回收可能會比手動釋放性能更好。即使性能差點,能擺脫內存泄露這樣的問題,還是非常值得的。

參考

http://en.wikipedia.org/wiki/Garbage_collection_(computer_science)

http://www.memorymanagement.org/glossary/t.html

http://www.hpl.hp.com/personal/Hans_Boehm/gc/

http://xtzgzorex.wordpress.com/2012/10/11/demystifying-garbage-collectors/


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章