JAVA進階之Reference

1、引用級別

在JVM中,垃圾回收器一直在背后默默地承擔(dān)著內(nèi)存回收的工作,我們不需要像使用C語音開發(fā)那樣小心翼翼地管理內(nèi)存。但是凡事皆有兩面性,這種機制的好處是極大地釋放了程序員無處安放的焦慮,壞處是難以對回收過程進行更靈活地干預(yù)。

為了增加對垃圾回收的力度把控,Java引入了引用級別的概念。

在JDK 1.2以前的版本中,只有在對象沒有任何其他對象引用它時,垃圾回收器才會對它進行收集。對象只有被引用和沒有被引用兩種狀態(tài)。這種方式無法描述一些“食之無味,棄之可惜”的對象。而很多時候,我們希望存在這樣一些對象:當(dāng)內(nèi)存空間足夠時,不進行回收;當(dāng)內(nèi)存空間變得緊張時,允許JVM回收這些對象。大部分緩存都符合這樣的場景。

從JDK 1.2版本開始,Java對引用的概念進行了擴充,對象的引用分成了4種級別,從而使程序開發(fā)者能更加靈活地控制對象的生命周期,更好的控制創(chuàng)建的對象何時被釋放和回收。

這4種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。

1.1 強引用(StrongReference)

我們使用的大部分的引用都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當(dāng)內(nèi)存空間不足,Java虛擬機寧愿拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內(nèi)存不足問題。

如果強引用對象不使用時,需要弱化從而使GC能夠回收,如:

strongReference = null;

顯式地設(shè)置strongReference對象為null,或讓其超出對象的生命周期范圍,則gc認(rèn)為該對象不存在引用,這時就可以回收這個對象。具體什么時候收集這要取決于GC算法。

public void test() {
    Object strongReference = new Object();
    // 省略其他操作
}

在一個方法的內(nèi)部有一個強引用,這個引用保存在Java棧中,而真正的引用內(nèi)容(Object)保存在Java堆中。 當(dāng)這個方法運行完成后,就會退出方法棧,則引用對象的引用數(shù)為0,這個對象會被回收。但是如果這個strongReference是全局變量時,就需要在不用這個對象時賦值為null,因為強引用不會被垃圾回收。

ArrayList的Clear方法:

public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

在ArrayList類中定義了一個elementData數(shù)組,在調(diào)用clear方法清空數(shù)組時,每個數(shù)組元素被賦值為null。 但是,這里為什么不直接將數(shù)組設(shè)置為null:

elementData=null;

一行代碼搞定,豈不更加簡單粗暴?
如果將整個數(shù)組指向null,該數(shù)組的內(nèi)存空間會被回收掉,而將數(shù)組的元素逐個指向null只會把數(shù)組中存放的強引用釋放,整個數(shù)組對象還是存在的,這樣以來,如果再有add()等操作就不用再次分配內(nèi)存了。

1.2 軟引用(SoftReference)

如果一個對象只具有軟引用,那就類似于可有可無的生活用品。如果內(nèi)存空間足夠,垃圾回收器就不會回收它,如果內(nèi)存空間不足了,就會回收這些對象的內(nèi)存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現(xiàn)內(nèi)存敏感的高速緩存。
軟引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對象被垃圾回收,JAVA虛擬機就會把這個軟引用加入到與之關(guān)聯(lián)的引用隊列中。

    ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
    String str = new String("abc");
    SoftReference<String> softReference = new SoftReference<>(str, referenceQueue);

    str = null;
    // Notify GC
    System.gc();

    System.out.println(softReference.get()); // abc

    Reference<? extends String> reference = referenceQueue.poll();
    System.out.println(reference); //null

注意: 軟引用對象是在jvm內(nèi)存不夠的時候才會被回收,我們調(diào)用System.gc()方法只是起通知作用,JVM什么時候掃描回收對象是JVM自己的狀態(tài)決定的。就算掃描到軟引用對象也不一定會回收它,只有內(nèi)存不夠的時候才會回收。
當(dāng)內(nèi)存不足時,JVM首先將軟引用中的對象引用置為null,然后通知垃圾回收器進行回收。也就是說,垃圾收集線程會在虛擬機拋出OutOfMemoryError之前回收軟引用對象,而且虛擬機會盡可能優(yōu)先回收長時間閑置不用的軟引用對象。對那些剛構(gòu)建的或剛使用過的“較新的”軟對象會被虛擬機盡可能保留,這就是引入引用隊列ReferenceQueue的原因。

1.3 弱引用(WeakReference)

弱引用與軟引用的區(qū)別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它 所管轄的內(nèi)存區(qū)域的過程中,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當(dāng)前內(nèi)存空間足夠與否,都會回收它的內(nèi)存。不過,由于垃圾回收器是一個優(yōu)先級很低的線程, 因此不一定會很快發(fā)現(xiàn)那些只具有弱引用的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關(guān)聯(lián)的引用隊列中。

    String str = new String("abc");
    WeakReference<String> weakReference = new WeakReference<>(str);
    System.out.println(weakReference.get() == null); //false
        
    str = null;
    System.gc(); //觸發(fā)垃圾回收
        
    System.out.println(weakReference.get() == null); //true

下面的代碼會讓一個弱引用再次變?yōu)橐粋€強引用:

    String str = new String("abc");
    WeakReference<String> weakReference = new WeakReference<>(str);
    // 弱引用轉(zhuǎn)強引用
    String strongReference = weakReference.get();

1.4 虛引用(PhantomReference)

"虛引用"顧名思義,就是形同虛設(shè),與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。

    String str = new String("abc");
    ReferenceQueue queue = new ReferenceQueue();
    // 創(chuàng)建虛引用,要求必須與一個引用隊列關(guān)聯(lián)
    PhantomReference pr = new PhantomReference(str, queue);

虛引用主要用來跟蹤對象被垃圾回收的活動。虛引用與軟引用和弱引用的一個區(qū)別在于:虛引用必須和引用隊列(ReferenceQueue)聯(lián)合使用。當(dāng)垃 圾回收器準(zhǔn)備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內(nèi)存之前,把這個虛引用加入到與之關(guān)聯(lián)的引用隊列中。程序可以通過判斷引用隊列中是 否已經(jīng)加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發(fā)現(xiàn)某個虛引用已經(jīng)被加入到引用隊列,那么就可以在所引用的對象的內(nèi)存被回收之前采取必要的行動。

2、java.lang.ref包

java.lang.ref包下都是與reference相關(guān)的類,包括:

1.png
  • Reference:引用對象的抽象基類,定義了所有引用對象的通用操作
  • ReferenceQueue: 引用隊列,垃圾回收器在檢測到對象可以回收時,會將該對象的Reference放到隊列(如果有)
  • SoftReference:Reference子類,代表軟引用
  • WeakReference:Reference子類,代表弱引用
  • PhantomReference:Reference子類,代表虛引用
  • FinalReference:Reference子類,訪問權(quán)限是package,外部無法訪問,由JVM來實例化,JVM會對那些實現(xiàn)了Object中finalize()方法的類實例化一個對應(yīng)的FinalReference
  • Finalizerd:FinalReference子類,訪問權(quán)限是package,且為final類,外部無法訪問。JVM實際操作的是Finalizer。當(dāng)一個類滿足實例化FinalReference的條件時,JVM會調(diào)用Finalizer.register()進行注冊

3、Reference

3.1 內(nèi)部變量

先來看一下Reference類中的變量

    private T referent;  

    volatile ReferenceQueue<? super T> queue;
    Reference next;

    private static Reference<Object> pending = null;
    transient private Reference<T> discovered;

    static private class Lock { }
    private static Lock lock = new Lock(); 
  • referent

表示其引用的對象,即在構(gòu)造的時候需要被包裝在其中的對象。

  • queue

是對象即將被回收時所要通知的隊列。當(dāng)對象即將被回收時,整個reference對象,而不僅僅是被回收的對象,會被放到queue 里面,然后外部程序通過監(jiān)控這個 queue 即可拿到相應(yīng)的數(shù)據(jù)了。ReferenceQueue并不是一個鏈表數(shù)據(jù)結(jié)構(gòu),它只持有這個鏈表的表頭對象header,這個鏈表是由Refence對象里面的next變量構(gòu)建起來,next也就是鏈表當(dāng)前節(jié)點的下一個節(jié)點,所以Reference對象本身就是一個鏈表的節(jié)點,這個鏈表由ReferenceHander線程從pending隊列中取出的數(shù)據(jù)構(gòu)建而來

  • next

與queue 搭配使用,僅在Reference放到queue中時才有意義,一系列的Reference正是依靠next組成了一個單鏈表

  • pending

pending與后面的discovered一起構(gòu)成了一個pending單向鏈表。注意這個變量是一個靜態(tài)對象,所以是全局唯一的。pending為鏈表的頭節(jié)點,discovered為鏈表當(dāng)前Reference節(jié)點指向下一個節(jié)點的引用,這個隊列是由JVM構(gòu)建的,當(dāng)對象除了被reference引用之外沒有其它強引用了,JVM就會將指向需要回收的對象的Reference都放入到這個隊列里面(注意是指向要回收的對象的Reference,要回收的對象就是Reference的成員變量refernt持有的對象,是refernt持有的對象要被回收,而不是Reference對象本身),這個隊列會由ReferenceHander線程來處理。

  • discovered

表示ReferenceHander要處理的pending對象的下一個對象。ReferenceHander只需要不停地拿到pending,然后再通過discovered 不斷地拿到下一個對象賦值給pending即可,直到取到了最有一個。

  • lock

lock是pending隊列的全局鎖,只在ReferenceHander線程的run方法里面用到。但是,除了當(dāng)前線程,JVM垃圾回收器線程也會操作pending隊列,所以需要通過這個鎖來防止并發(fā)問題。

3.2 ReferenceHander

ReferenceHanderReference的一個內(nèi)部類,繼承自Thread:

    private static class ReferenceHandler extends Thread {

        private static void ensureClassInitialized(Class<?> clazz) {
            try {
                Class.forName(clazz.getName(), true, clazz.getClassLoader());
            } catch (ClassNotFoundException e) {
                throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
            }
        }

        static {
           // 確保依賴的其他類已被加載
            ensureClassInitialized(InterruptedException.class);
            ensureClassInitialized(Cleaner.class);
        }

        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }

        public void run() {
            while (true) {
                //啟動后無限循環(huán),調(diào)用tryHandlePending方法
                tryHandlePending(true);
            }
        }
    }

    static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            //此處需要加全局鎖,因為除了當(dāng)前線程,gc線程也會操作pending隊列
            synchronized (lock) {
                //如果pending隊列不為空,則將第一個Reference對象取出
                if (pending != null) {
                    r = pending;
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    //將頭節(jié)點指向discovered,即隊列中的下一個節(jié)點,這樣就把第一個頭結(jié)點出隊了
                    pending = r.discovered;
                     //將當(dāng)前節(jié)點的discovered設(shè)置為null;當(dāng)前節(jié)點已出隊,不再需要鏈表序列
                    r.discovered = null;
                } else {
                    //如果pending隊列為空且需要等待,則進入阻塞狀態(tài)
                    if (waitForNotify) {
                        lock.wait();
                    }
                   
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            Thread.yield();
            return true;
        } catch (InterruptedException x) {
            return true;
        }

        // 如果從pending隊列出隊的是一個Cleaner對象,那么直接執(zhí)行其clean()方法
        if (c != null) {
            c.clean();
            //注意這里,這里已經(jīng)不往下執(zhí)行了,所以Cleaner對象是不會進入到隊列里面的,給它設(shè)置ReferenceQueue的作用是為了讓它能進入Pending隊列后被ReferenceHander線程處理
            return true;
        }

        //如果綁定了ReferenceQueue隊列,則將對象插入其中
        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }

可見,ReferenceHander的任務(wù)就是將pending隊列中要被回收的Reference對象移除出來,如果Reference對象在初始化的時候傳入了ReferenceQueue隊列,那么就把從pending隊列里面移除的Reference放到它自己的ReferenceQueue隊列里,如果沒有ReferenceQueue隊列,那么其關(guān)聯(lián)的對象就不會進入到Pending隊列中,會直接被回收掉。

如果把pending與discovered看做指針,ReferenceHander操作pending隊列的流程可以簡化為下圖:

2.jpg

那么問題來了:

1、Reference 源碼中并沒有發(fā)現(xiàn)給 discovered和 pending 賦值的地方,是誰負(fù)責(zé)它們的初始化呢?

2、ReferenceHandler是什么時候由誰啟動的?

問題1,pending隊列由JVM的垃圾回收器維護,負(fù)責(zé)初始化discovered和 pending,并不斷將新的Reference追加到隊列當(dāng)中。

問題2,去源碼中尋找答案,Reference類中有一段靜態(tài)代碼:

    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        //擁有最高優(yōu)先級
        handler.setPriority(Thread.MAX_PRIORITY); 
        //設(shè)為守護線程
        handler.setDaemon(true);
        //啟動線程
        handler.start();

        // provide access in SharedSecrets
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
    }

ReferenceHandler是一個擁有最高優(yōu)先級的守護線程,在Reference類加載執(zhí)行cinit的時候被初始化并啟動。

3.3 Reference狀態(tài)及其轉(zhuǎn)換

Reference源碼開頭有一段很長的注釋,說明了Reference對象的四種狀態(tài):

  • Active

活動狀態(tài),對象存在強引用狀態(tài),還沒有被回收

  • Pending

垃圾回收器將沒有強引用的Reference對象放入到pending隊列中,等待ReferenceHander線程處理(前提是這個Reference對象創(chuàng)建的時候傳入了ReferenceQueue,否則的話對象會直接進入Inactive狀態(tài))

  • Enqueued

:ReferenceHander線程將pending隊列中的對象取出來放到ReferenceQueue隊列里

  • Inactive

處于此狀態(tài)的Reference對象可以被回收,并且其內(nèi)部封裝的對象也可以被回收掉了,有兩個路徑可以進入此狀態(tài):

路徑一:在創(chuàng)建時沒有傳入ReferenceQueue的Reference對象,被Reference封裝的對象在沒有強引用時,指向它的Reference對象會直接進入此狀態(tài);

路徑二:此Reference對象經(jīng)過前面三個狀態(tài)后,已經(jīng)由外部從ReferenceQueue中獲取到,并且已經(jīng)處理掉了。

狀態(tài)的轉(zhuǎn)換示意圖如下:

3.jpg

從代碼層面來說,Reference對象的狀態(tài)只需要通過成員變量next和queue來判斷:

  • Active:next=null
  • Pending:next = this, queue = ReferenceQueue
  • Enqueued:queue =ReferenceQueue.ENQUEUED
  • Inactive:next = this, queue = ReferenceQueue.NULL;

可以結(jié)合后面的ReferenceQueue源碼加深理解。

4、 ReferenceQueue

4.1 內(nèi)部變量

    private static class Null<S> extends ReferenceQueue<S> {
        boolean enqueue(Reference<? extends S> r) {
            return false;
        }
    }   
    static ReferenceQueue<Object> NULL = new Null<>();
    static ReferenceQueue<Object> ENQUEUED = new Null<>();

    static private class Lock { };
    private Lock lock = new Lock();

    //隊列頭
    private volatile Reference<? extends T> head = null;
    //隊列長度
    private long queueLength = 0;

ReferenceQueue定義了一個內(nèi)部類Null<S>,重寫了enqueue入隊方法,永遠(yuǎn)返回false,所以它不會存儲任何數(shù)據(jù),是一個用來做狀態(tài)識別的空隊列。
成員變量NULLENQUEUED是內(nèi)部類的兩個實例,用以標(biāo)識Reference的狀態(tài)。當(dāng)Reference對象創(chuàng)建時沒有指定queue或Reference對象已經(jīng)處于inactive狀態(tài)時,其持有的queue會指向NULL;當(dāng)Reference被ReferenceHander線程從pending隊列移到queue里面時,其持有的queue會指向ENQUEUED。

與Reference類似,ReferenceQueue內(nèi)部也定義了一個Lock類,內(nèi)部沒有任何代碼填充,就是一個標(biāo)志類,用來充當(dāng)鎖的載體。但是,Reference中的Reference中的lock變量是靜態(tài)的,也就是被所有實例共享的;而ReferenceQueue中的lock變量是非靜態(tài)的,也就是每個ReferenceQueue實例獨占一份,這是為什么呢?
原因在于,Reference中的lock用于鎖定pending隊列,pending隊列本身就是靜態(tài)變量,全局唯一,lock變量當(dāng)然也得是靜態(tài)的;而ReferenceQueue是與某個具體的Reference綁定,入隊、出隊等操作對于每個ReferenceQueue實例來說都是相互獨立的,只需要鎖定實例相關(guān)的隊列即可,所以lock是實例變量,而非靜態(tài)變量

head變量持有隊列的隊頭,head與Reference中的next變量構(gòu)成了一個單鏈表。Reference對象是從隊頭做出隊入隊操作,所以它是一個后進先出的隊列,其實在數(shù)據(jù)結(jié)構(gòu)上更像更像一個棧。

4.2 入隊操作

    boolean enqueue(Reference<? extends T> r) { 
        //首先要鎖定隊列
        synchronized (lock) { 
            ReferenceQueue<?> queue = r.queue;
            //如果Reference沒有綁定隊列,或者已經(jīng)入隊,直接返回
            if ((queue == NULL) || (queue == ENQUEUED)) {
                return false;
            }
            assert queue == this;
            //將Reference設(shè)為已入隊狀態(tài)
            r.queue = ENQUEUED;
            //如果隊列為空,Reference的next指向自己(自己將會成為隊頭);否則,指向隊頭
            r.next = (head == null) ? r : head;
            //將自己設(shè)為隊頭
            head = r;
            //增加隊列長度
            queueLength++;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(1);
            }
            //喚醒出隊操作的等待線程
            lock.notifyAll();
            return true;
        }
    }

入隊的操作很簡單,就是不斷地將新對象插入到隊頭,流程示意圖如下:

4.jpg

4.3 出隊操作

有兩種出隊操作:

  • poll:非阻塞式,隊列中沒有對象直接返回
  • remove:阻塞式,隊列中沒有對象時阻塞線程,直到有新對象進來,可以設(shè)置最長的阻塞時間
    //真正執(zhí)行出隊操作的方法,poll與remove都會調(diào)用
    private Reference<? extends T> reallyPoll() { 
        //獲取隊頭
        Reference<? extends T> r = head;
        if (r != null) {
            //將隊頭重置為下一個對象
            head = (r.next == r) ?
                null :
                r.next; 
            //出隊的reference設(shè)為已出隊狀態(tài)
            r.queue = NULL;
            //next指向自己
            r.next = r;
            //減少隊列長度
            queueLength--;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(-1);
            }
            return r;
        }
        //隊列為空,則直接返回
        return null;
    }

    //非阻塞式出隊
    public Reference<? extends T> poll() {
        if (head == null)
            return null;
        synchronized (lock) {
            return reallyPoll();
        }
    }

    //阻塞式出隊
    public Reference<? extends T> remove(long timeout)
        throws IllegalArgumentException, InterruptedException
    {
        if (timeout < 0) {
            throw new IllegalArgumentException("Negative timeout value");
        }
        synchronized (lock) {
            Reference<? extends T> r = reallyPoll();
            //出隊成功,返回結(jié)果
            if (r != null) return r;
            long start = (timeout == 0) ? 0 : System.nanoTime();
            for (;;) {
                //出隊失敗,阻塞線程一定時間
                lock.wait(timeout);
                //期間如果被喚醒,會重試出隊操作
                r = reallyPoll();
                //出隊成功,返回結(jié)果
                if (r != null) return r;
                //重試仍然失敗,如果未超時,繼續(xù)循環(huán)
                if (timeout != 0) {
                    long end = System.nanoTime();
                    timeout -= (end - start) / 1000_000;
                    if (timeout <= 0) return null;
                    start = end;
                }
            }
        }
    }

出隊的流程示意圖如下:

6.jpg

5、 SoftReference、WeakReference和PhantomReference

SoftReference、WeakReference和PhantomReference是reference的三個子類,主要功能都已在父類定義,無需贅言。
值得額外提一句的是SoftReference,相較于其他子類,其特殊的地方在于多了兩個內(nèi)部變量:clocktimestamp

    //由垃圾回收器維護的一個時鐘
    static private long clock;

    //每次調(diào)用get方法時,會將使用lock更新這個時間戳
    private long timestamp;

    public T get() {
        T o = super.get();
        if (o != null && this.timestamp != clock)
            this.timestamp = clock;
        return o;
    }

clock是一個靜態(tài)變量,由垃圾回收器負(fù)責(zé)維護。

timestamp是非靜態(tài)變量,初始值等于clock,并且在每次get的時候更新,JVM會參考timestamp來決定是否回收該引用。

6、 FinalReference和Finalizer

6.1 Finalizer機制

Java有垃圾回收器負(fù)責(zé)回收無用的內(nèi)存空間,但JVM只能管理自己的內(nèi)存空間,對于應(yīng)用運行時需要的其它native資源(jvm通過jni暴漏出來的功能):例如直接內(nèi)存DirectByteBuffer,網(wǎng)絡(luò)連接SocksSocketImpl,文件流FileInputStream等與操作系統(tǒng)有交互的資源,JVM就無能為力了,需要我們自己來調(diào)用釋放資源的方法。

但是人在計算機世界中是個最不靠譜的因素,一旦程序員沒有手動釋放這些資源,豈不會導(dǎo)致資源泄露?為了幫助愚蠢的人類,Java提供了finalizer機制:如果對象實現(xiàn)了Object.finalize()方法,JVM會在回收對象之前調(diào)用該方法,釋放掉外部資源。
例如,F(xiàn)ileInputStream的finalize():

    protected void finalize() throws IOException {
        if ((fd != null) &&  (fd != FileDescriptor.in)) {
            /* if fd is shared, the references in FileDescriptor
             * will ensure that finalizer is only called when
             * safe to do so. All references using the fd have
             * become unreachable. We can call close()
             */
            close();
        }
    }

SocksSocketImpl則在父類AbstractPlainSocketImpl中實現(xiàn)了finalize()方法:

    protected void finalize() throws IOException {
        close();
    }

6.2 unfinalized隊列

Finalizer機制與FinalReference類、Finalizer類密切相關(guān)。

class FinalReference<T> extends Reference<T> {
    public FinalReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}
final class Finalizer extends FinalReference<Object> { 

    private Finalizer(Object finalizee) {
        super(finalizee, queue);
        add();
    }
    //...
}

Finalizer繼承FinalReference類,F(xiàn)inalReference繼承Reference類。

FinalReference和Finalizer的訪問權(quán)限是package的,意味著我們不能直接去對其進行擴展。此外,F(xiàn)inalizer類還是final的,構(gòu)造函數(shù)是private的,JDK把它包裝的如此嚴(yán)密,看來不想讓我們過多染指,爾等未開化的草民看看就行了。

先來看下Finalizer類的內(nèi)部變量:

    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
    private static Finalizer unfinalized = null;
    private static final Object lock = new Object();

    private Finalizer
        next = null,
        prev = null;

首先定義了一個ReferenceQueue,會通過Finalizer的構(gòu)造函數(shù)傳給父類,實際上最終充當(dāng)了Reference中的queue變量。值得注意的是,這個queue是是靜態(tài)的,也就是說所有Finalizer共享同一個queue。
此外,還定義了一個慣用的Lock,一個名為unfinalized的變量,兩者也都是靜態(tài)的。
再往下看,又出現(xiàn)了兩個私有變量,一個名為next,一個名為prev,顧名思義,隱約嗅到了一絲雙端鏈表的味道。

    //添加節(jié)點到雙端鏈表
    private void add() {
        //第一步肯定要鎖住鏈表
        synchronized (lock) {
            //鏈表不為空,將表頭置為自己的后繼節(jié)點
            if (unfinalized != null) {
                this.next = unfinalized;
                unfinalized.prev = this;
            }
            //表頭指向自己
            unfinalized = this;
        }
    }
    //從雙端鏈表中刪除節(jié)點
    private void remove() {
        //拿鎖
        synchronized (lock) {
            //如果表頭是自己,需要先將表頭指向自己的后繼節(jié)點或前驅(qū)節(jié)點
            if (unfinalized == this) {
                if (this.next != null) {
                    unfinalized = this.next;
                } else {
                    unfinalized = this.prev;
                }
            }
            //取消自己與后繼節(jié)點的關(guān)聯(lián)
            if (this.next != null) {
                this.next.prev = this.prev;
            }
             //取消自己與前驅(qū)節(jié)點的關(guān)聯(lián)
            if (this.prev != null) {
                this.prev.next = this.next;
            }
            //后繼節(jié)點與前驅(qū)節(jié)點都指向自己(代表節(jié)點已移除)
            this.next = this;   
            this.prev = this;
        }
    }

果然如此,F(xiàn)inalizer類實現(xiàn)了雙端鏈表的增加、刪除等方法,unfinalized指向表頭,lock用于鎖定鏈表,nextprev分別指向節(jié)點的后繼與前驅(qū)。

添加示意圖:

7.jpg

刪除示意圖:

8.jpg

6.3 f類

類的修飾有很多,比如final,abstract,public等,如果某個類用final修飾,我們就說這個類是final類,上面列的都是語法層面我們可以顯式指定的,在JVM里其實還會給類標(biāo)記一些其他符號,比如finalizer,表示這個類是一個finalizer類(為了和java.lang.ref.Fianlizer類區(qū)分,finalizer類簡稱為f類),GC在處理這種類的對象時要做一些特殊的處理,如在這個對象被回收之前會調(diào)用它的finalize方法。

JVM在類加載的時候會遍歷當(dāng)前類的所有方法,包括父類的方法,如果有一個方法滿足一下條件,該類就會被標(biāo)記為f類:

  • 當(dāng)前類或其父類含有一個參數(shù)為空,返回值為void,名為finalize的方法;
  • 這個finalize方法體不能為空;

java.lang.Object里就有一個finalize()方法:

protected void finalize() throws Throwable { }

但是由于其方法體為空,Object并不是一個f類。

6.4 f類的注冊

那么jvm又是在何時調(diào)用register方法的呢?

對象的創(chuàng)建其實是被拆分成多個步驟的,比如A a=new A(2)這樣一條語句對應(yīng)的字節(jié)碼如下:

0: new           #1                  // class A
3: dup
4: iconst_2
5: invokespecial #11                 // Method "<init>":(I)V

先執(zhí)行new分配好對象空間,然后再執(zhí)行invokespecial調(diào)用構(gòu)造函數(shù)。JVM何時調(diào)用Finalizer.register方法,取決于參數(shù)RegisterFinalizersAtInit。改參數(shù)默認(rèn)值為true,代表在構(gòu)造函數(shù)返回之前調(diào)用Finalizer.register方法,如果通過-XX:-RegisterFinalizersAtInit關(guān)閉了該參數(shù),那將在對象空間分配好之后將這個對象注冊進去。
當(dāng)我們通過clone的方式復(fù)制一個對象時,如果當(dāng)前類是一個f類,那么在clone完成時將調(diào)用Finalizer.register方法進行注冊。

6.5 FinalizerThread線程

在Finalizer類的靜態(tài)塊里會創(chuàng)建一個FinalizerThread守護線程,這個線程的優(yōu)先級并不是最高的,意味著在CPU很緊張的情況下其被調(diào)度的優(yōu)先級可能會受到影響。

static {
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    Thread finalizer = new FinalizerThread(tg);
    //現(xiàn)成優(yōu)先級設(shè)為8,高于普通現(xiàn)成的優(yōu)先級5
    finalizer.setPriority(Thread.MAX_PRIORITY - 2);
    //設(shè)為守護線程
    finalizer.setDaemon(true);
    finalizer.start();
}

private static class FinalizerThread extends Thread {
    private volatile boolean running;
    FinalizerThread(ThreadGroup g) {
        super(g, "Finalizer");
    }
    public void run() {
        //如果run方法已經(jīng)在執(zhí)行了,直接退出
        if (running)
            return;

        //等待jvm初始化完成后才繼續(xù)執(zhí)行
        while (!VM.isBooted()) {
            // delay until VM completes initialization
            try {
                VM.awaitBooted();
            } catch (InterruptedException x) {
                // ignore and continue
            }
        }
        final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
        running = true;
        for (;;) {
            try {
                //將對象從ReferenceQueue中移除,
                Finalizer f = (Finalizer)queue.remove();
                //通過runFinalizer調(diào)用finalizer方法
                f.runFinalizer(jla);
            } catch (InterruptedException x) {
                // ignore and continue
            }
        }
    }
}

private void runFinalizer(JavaLangAccess jla) {
    synchronized (this) {
        //若next==this,則表明this對象已經(jīng)從unfinalized對象鏈中移除,已經(jīng)執(zhí)行過一次runFinalizer了
        if (hasBeenFinalized()) return;
        //將該對象從unfinalized隊列中移除  
        remove();
    }
    try {
        Object finalizee = this.get();
        if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
            //通過JDK調(diào)用對象的finalize方法
            jla.invokeFinalize(finalizee);
            finalizee = null;
        }
    } catch (Throwable x) { }
    super.clear();
}

private boolean hasBeenFinalized() {
    return (next == this);
}

這個線程用來從queue里獲取Finalizer對象,然后執(zhí)行該對象的runFinalizer方法,該方法會將Finalizer對象從unfinalized隊列里剝離出來,這樣意味著下次GC發(fā)生時就可以將其關(guān)聯(lián)的f對象回收了,最后調(diào)用f類的finalized方法。

至此,F(xiàn)inalizer的整個流程打通了。從頭再捋一遍:

  1. 當(dāng)GC發(fā)生時,JVM會判斷f類是否只被Finalizer類引用;

  2. 若這個類只被Finalizer對象引用,說明這個對象在不久的將來會被回收,現(xiàn)在可以執(zhí)行他的finalize方法了;

  3. 將f類放到Finalizer類的ReferenceQueue中,但這個f類對象其實并沒有被回收,因為Finalizer這個類還對他們保持引用;

  4. GC完成之前,JVM會調(diào)用ReferenceQueue中l(wèi)ock對象的notify方法;

  5. Finalizer的守護線程可能會被喚醒,從Queue中取出對象(remove),執(zhí)行該Finalizer對象的runFinalizer方法(將自己從unfinalized隊列移除,然后執(zhí)行引用對象的finalize方法)

  6. 下次GC時回收這個對象。

6.6 Finalizer類的評價

  • f類因為Finalizer的引用而變成了一個臨時的強引用,即使沒有其他的強引用,還是無法立即被回收;

  • f類至少經(jīng)歷兩次GC才能被回收,因為只有在FinalizerThread執(zhí)行完了f對象的finalize方法的情況下才有可能被下次GC回收,而有可能期間已經(jīng)經(jīng)歷過多次GC了,但是一直還沒執(zhí)行f對象的finalize方法;

  • f對象的finalize方法被調(diào)用后,這個對象其實還并沒有被回收,雖然可能在不久的將來會被回收。

  • Finalizer線程是一個單線程來處理f-queue,雖然可以再啟動第二個(forkSecondaryFinalizer()),但是也是兩個線程而已,如果系統(tǒng)中有很多線程爭用cpu,在系統(tǒng)壓力比較大的情況下,F(xiàn)inalizer線程獲取到cpu時間片的時間是不確定的,在其獲取到時間片之前,應(yīng)該被回收的Finalizer對象一直在隊列中積累,占用大量內(nèi)存,經(jīng)過n次gc后,仍然沒有機會被釋放掉,這些對象都進入到老年代,導(dǎo)致old剩余空間變小,從而使fullgc會更加頻繁,如果Finalizer對象積壓嚴(yán)重的甚至?xí)?dǎo)致oom;

  • 如果Finalizer對象生產(chǎn)的速度比Finalizer線程處理的速度要快,也會導(dǎo)致f-queue隊列里面的Finalizer對象積壓,這些對象一直占用jvm的內(nèi)存,直到oom;

  • 如果執(zhí)行某個f類的finalizer方法執(zhí)行非常耗時,或這個方法里面的操作被鎖阻塞了Finalizer線程,那么就會導(dǎo)致隊列里面其它的Finalizer對象一直在等待隊列里面無法被回收釋放空間,最終導(dǎo)致oom;

  • Reference對象是在gc的時候來處理的,如果沒有觸發(fā)GC就沒有機會觸發(fā)Reference引用的處理操作,那么應(yīng)該被回收的FinalReference對象就一直在unfinalized隊列里,無法被回收,導(dǎo)致被它引用的對象也無法回收,然后又導(dǎo)致被引用對象占用的資源也不會釋放,最終可能會導(dǎo)致native資源耗盡;

  • 可能導(dǎo)致資源泄露,例如當(dāng)jvm退出時,很可能unfinalizer隊列里的對象沒有被處理完就退出了;

  • 對象有可能在執(zhí)行過finalize方法后,又被強引用引用到了,于是對象就復(fù)活了;

一句話總結(jié):盡量不要使用Finalizer類,釋放資源一定要手動去釋放,如果忘記釋放,依靠finalizer的機制是不靠譜的,很可能會導(dǎo)致一些嚴(yán)重的內(nèi)存問題或native資源泄露問題;如果一定要用,必須保證調(diào)用finalize方法能夠快速執(zhí)行完成。

7、 Cleaner

另外java里面還有一個sun.misc.Cleaner類,它繼承自PhantomReference,作用同F(xiàn)inalize一樣,它的清理工作是在ReferenceHandel線程里面完成的,只是少了Finalizer線程處理這一步,F(xiàn)inalize存在的問題,它基本都有,如果clean方法使用不當(dāng),阻塞ReferenceHander線程,會導(dǎo)致比finalizer線程更加嚴(yán)重的問題。

public class Cleaner
    extends PhantomReference<Object>
{

    // Dummy reference queue, needed because the PhantomReference constructor
    // insists that we pass a queue.  Nothing will ever be placed on this queue
    // since the reference handler invokes cleaners explicitly.
    // 就像英文注釋所說的,這貨沒啥卵用
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();

    // Doubly-linked list of live cleaners, which prevents the cleaners
    // themselves from being GC'd before their referents
    // 所有的cleaner都會被加到一個雙向鏈表中去,這樣做是為了保證在referent被回收之前
    // 這些Cleaner都是存活的。
    static private Cleaner first = null;

    private Cleaner
        next = null,
        prev = null;

    // 構(gòu)造的時候把自己加到雙向鏈表中去
    private static synchronized Cleaner add(Cleaner cl) {
        if (first != null) {
            cl.next = first;
            first.prev = cl;
        }
        first = cl;
        return cl;
    }

    // clean方法會調(diào)用remove把當(dāng)前的cleaner從鏈表中刪除。
    private static synchronized boolean remove(Cleaner cl) {
        // If already removed, do nothing
        if (cl.next == cl)
            return false;

        // Update list
        if (first == cl) {
            if (cl.next != null)
                first = cl.next;
            else
                first = cl.prev;
        }
        if (cl.next != null)
            cl.next.prev = cl.prev;
        if (cl.prev != null)
            cl.prev.next = cl.next;

        // Indicate removal by pointing the cleaner to itself
        cl.next = cl;
        cl.prev = cl;
        return true;
    }

    // 用戶自定義的一個Runnable對象,
    private final Runnable thunk;

    // 私有有構(gòu)造函數(shù),保證了用戶無法單獨地使用new來創(chuàng)建Cleaner。
    private Cleaner(Object referent, Runnable thunk) {
        super(referent, dummyQueue);
        this.thunk = thunk;
    }

    /**
     * 所有的Cleaner都必須通過create方法進行創(chuàng)建。
     */
    public static Cleaner create(Object ob, Runnable thunk) {
        if (thunk == null)
            return null;
        return add(new Cleaner(ob, thunk));
    }

    /**
     * 這個方法會被Reference Handler線程調(diào)用,來清理資源。
     */
    public void clean() {
        if (!remove(this))
            return;
        try {
            thunk.run();
        } catch (final Throwable x) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null)
                            new Error("Cleaner terminated abnormally", x)
                                .printStackTrace();
                        System.exit(1);
                        return null;
                    }});
        }
    }
}

Cleaner本身不帶有清理邏輯,所有的邏輯都封裝在thunk參數(shù)中,通過構(gòu)造函數(shù)傳入,因此thunk是怎么實現(xiàn)的才是最關(guān)鍵的。

JDK中的DirectByteBuffer就是使用Cleaner清理的,來看一下它得實現(xiàn):

   DirectByteBuffer(int cap) {// package-private

        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            //通過unsafe分配內(nèi)存
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        //構(gòu)建Cleaner實例
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }


    private static class Deallocator
        implements Runnable
    {

        private static Unsafe unsafe = Unsafe.getUnsafe();

        private long address;
        private long size;
        private int capacity;

        private Deallocator(long address, long size, int capacity) {
            assert (address != 0);
            this.address = address;
            this.size = size;
            this.capacity = capacity;
        }

        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            //通過unsafe釋放內(nèi)存
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }

    }

可見,DirectBuffer的內(nèi)存釋放通過Cleaner調(diào)用unsafe類來實現(xiàn)。不過DirectBuffer的釋放時機還是不確定的。首先,得發(fā)生GC,其次,Reference Handler得調(diào)度到,然后處理到你的cleaner才行。

如果要實現(xiàn)一個cleaner,萬萬不要在run方法里寫一些執(zhí)行時間很長,或者會阻塞線程的邏輯的,會把Reference Handler拖死。

由于Finalizer存在上文提到的諸多問題,Java 9中finalize方法已經(jīng)被廢棄,新增java.lang.ref.Cleaner類來提供更靈活、有效的資源釋放方式。這個新的 java.lang.ref.Cleaner 其實是以前的 sun.misc.Cleaner 的公有API移植版。Cleaner 是基于 PhantomReference 的,所以不會像finalizer那樣有復(fù)活對象的機會,所以坑會比finalizer稍微少一點。不過,也就是稍微少一點而已。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 感知GC。怎么感知:* 通過get來判斷已經(jīng)被GC(PhantomReference 在任何時候get都是null...
    YDDMAX_Y閱讀 1,979評論 0 4
  • 1 簡介 Reference是所有引用類型的父類,定義了引用的公共行為和操作, 2 Reference類結(jié)構(gòu) Re...
    貪睡的企鵝閱讀 1,098評論 0 0
  • 引用的分類 Java 1.2以后,除了普通的引用外,Java還定義了軟引用、弱引用、虛引用等概念。 強引用:GC ...
    劉惜有閱讀 942評論 0 1
  • JDK1.2之后,Java擴充了引用的概念,將引用分為強引用、軟引用、弱引用和虛引用四種。 強引用類似于”O(jiān)bje...
    lesline閱讀 4,977評論 0 0
  • 誰不想活的風(fēng)聲水起,誰不想活的隨心所欲,問題是怎么樣才可以呢?貧窮限制了我們的想象,有時候也束縛了我們頭腦,關(guān)鍵是...
    寸丹心閱讀 236評論 0 2

友情鏈接更多精彩內(nèi)容