垃圾收集器:引用計數演算法

五柳-先生發表於2016-06-07

引用計數演算法作為垃圾收集器最早的演算法,有其優勢,也有其劣勢,雖然現在的JVM都不再採用引用計數演算法進行垃圾回收【例如SunJava hotspot採用了火車演算法進行垃圾回收】,但這種演算法也並未被淘汰,在著名的單程式高併發快取Redis中依然採用這種演算法來進行記憶體回收【後緒會以Redis作為例子,說明該演算法】

什麼是引用計數演算法

直白一點,就是對於建立的每一個物件都有一個與之關聯的計數器,這個計數器記錄著該物件被使用的次數,垃圾收集器在進行垃圾回收時,對掃描到的每一個物件判斷一下計數器是否等於0,若等於0,就會釋放該物件佔用的記憶體空間,同時將該物件引用的其他物件的計數器進行減一操作

兩種實現方式

侵入式與非侵入性,引用計數演算法的垃圾收集一般有侵入式與非侵入式兩種,侵入式的實現就是將引用計數器直接根植在物件內部,用C++的思想進行解釋就是,在物件的構造或者拷貝構造中進行加一操作,在物件的析構中進行減一操作,非侵入式恩想就是有一塊單獨的記憶體區域,用作引用計數器

演算法的優點

使用引用計數器,記憶體回收可以穿插在程式的執行中,在程式執行中,當發現某一物件的引用計數器為0時,可以立即對該物件所佔用的記憶體空間進行回收,這種方式可以避免FULL GC時帶來的程式暫停,如果讀過Redis 1.0的原始碼,可以發現Redis中就是在引用計數器為0時,對記憶體進行了回收

演算法的劣勢

採用引用計數器進行垃圾回收,最大的缺點就是不能解決迴圈引用的問題,例如一個父物件持有一個子物件的引用,子物件也持有父物件的引用,這種情況下,父子物件將一直存在於JVM的堆中,無法進行回收,程式碼示例如下所示(引用計數器無法對a與b物件進行回收):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class A {
    private B b;
    public B getB() {
        return b;
    }
    public void setB(B b) {
        this.b = b;
    }
}
 
class B {
    private A a;
    public A getA() {
        return a;
    }
    public void setA(A a) {
        this.a = a;
    }
}
 
public class Test {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        a.setB(b);
        b.setA(a);
    }
}

如下是Redis 1.0通過使用引用計數器對記憶體進行回收的

1
2
3
4
5
6
7
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    int refcount;//引用計數器
    void *ptr;//指向實際的物件空間
} robj;

Redis中所有的操作,操作的都是robj這個結構體,在這個結構中存放著物件的引用計數器refcount,如下是建立物件的程式碼,在這個建立物件的過程中,將引用計數器置為1

1
2
3
4
5
6
7
8
9
10
11
robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = REDIS_ENCODING_RAW;
    o->ptr = ptr;
    //建立時將引用計數器初始為1
    o->refcount = 1;
    /* Set the LRU to the current lruclock (minutes resolution). */
    o->lru = LRU_CLOCK();
    return o;
}

以下操作是對引用計數器進行+1操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
robj *createStringObjectFromLongLong(long long value) {
    robj *o;
    if (value >= 0 && value < REDIS_SHARED_INTEGERS) {
        //對共享池中常量物件的引用計數+1
        incrRefCount(shared.integers[value]);
        o = shared.integers[value];
    else {
        if (value >= LONG_MIN && value <= LONG_MAX) {
            o = createObject(REDIS_STRING, NULL);
            o->encoding = REDIS_ENCODING_INT;
            o->ptr = (void*)((long)value);
        else {
            o = createObject(REDIS_STRING,sdsfromlonglong(value));
        }
    }
    return o;
}

Redis中有一個共享池,共享池中的變數,一般不會輕易釋放,大部份物件都可以對這部份常量進行共享,共享一次,對應物件robj中的引用計數器進行一次+1操作

以下是進行-1操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void decrRefCount(robj *o) {
    if (o->refcount <= 0) redisPanic("decrRefCount against refcount <= 0");
    if (o->refcount == 1) {
        switch(o->type) {
        case REDIS_STRING: freeStringObject(o); break;
        case REDIS_LIST: freeListObject(o); break;
        case REDIS_SET: freeSetObject(o); break;
        case REDIS_ZSET: freeZsetObject(o); break;
        case REDIS_HASH: freeHashObject(o); break;
        default: redisPanic("Unknown object type"); break;
        }
        zfree(o);
    else {
        o->refcount--;
    }
}

從上面的程式碼中可以看出,對物件的引用計數器進行-1操作時,如果物件的引用計數器變為0時,會呼叫相應型別的釋放函式,釋放物件的記憶體空間,如果物件的引用計數器的值大於1時,直接對物件的引用計數器進行減1操作,然後返回

從上面的程式碼可以看出,Redis中通過對物件的引用計數器進行減1操作,可以實現在程式執行過程中,回收物件所佔用的記憶體空間,當然Redis中還有LRU演算法,實現記憶體淘汰策略,待以後再分析

Redis 1.0原始碼註解:https://github.com/zwjlpeng/Redis_Deep_Read

http://www.cnblogs.com/WJ5888/p/4359783.html

相關文章