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


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

什么是引用計數算法

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

兩種實現方式

侵入式與非侵入性,引用計數算法的垃圾收集一般有侵入式與非侵入式兩種,侵入式的實現就是將引用計數器直接根植在對象內部,用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通過使用引用計數器對內存進行回收的

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM