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

引用計數算法做爲垃圾收集器最先的算法,有其優點,也有其劣勢,雖然如今的JVM都再也不採用引用計數算法進行垃圾回收【例如SunJava hotspot採用了火車算法進行垃圾回收】,但這種算法也並未被淘汰,在著名的單進程高併發緩存Redis中依然採用這種算法來進行內存回收【後緒會以Redis做爲例子,說明該算法】java

什麼是引用計數算法git

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

兩種實現方式redis

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

算法的優勢緩存

使用引用計數器,內存回收能夠穿插在程序的運行中,在程序運行中,當發現某一對象的引用計數器爲0時,能夠當即對該對象所佔用的內存空間進行回收,這種方式能夠避免FULL GC時帶來的程序暫停,若是讀過Redis 1.0的源碼,能夠發現Redis中就是在引用計數器爲0時,對內存進行了回收併發

算法的劣勢函數

採用引用計數器進行垃圾回收,最大的缺點就是不能解決循環引用的問題,例如一個父對象持有一個子對象的引用,子對象也持有父對象的引用,這種狀況下,父子對象將一直存在於JVM的堆中,沒法進行回收,代碼示例以下所示(引用計數器沒法對a與b對象進行回收):高併發

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經過使用引用計數器對內存進行回收的this

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

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操做

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操做

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

相關文章
相關標籤/搜索