FinalReference詳解

朱清震發表於2018-02-01

前請先看《一提到Reference 99.99%的java程式設計師都懵逼了》,否則裡面的講解會看不懂!

前面一文將了Reference類,現在來看看FinalReference,相信大部分程式設計師都遇到過Finalizer物件佔用記憶體過高或導致oom的問題,這篇文章告訴你為什麼會出現這種問題:

為什麼需要FinalReference

因為jvm只能管理jvm記憶體空間,但是對於應用執行時需要的其它native資源(jvm通過jni暴漏出來的功能):例如直接記憶體DirectByteBuffer,網路連線SocksSocketImpl,檔案流FileInputStream等與作業系統有互動的資源,jvm就無能為力了,需要我們自己來呼叫釋放這些資源方法來釋放,為了避免物件死了之後,程式設計師忘記手動釋放這些資源,導致這些物件有的外部資源洩露,java提供了finalizer機制通過重寫物件的finalizer方法,在這個方法裡面執行釋放物件佔用的外部資源的操作,這樣使用這些資源的程式設計師即使忘記手動釋放,jvm也可以在回收物件之前幫助釋放掉這些外部資源,幫助我們呼叫這個方法回收資源的執行緒就是我們在匯出jvm執行緒棧時看到的名為Finalizer的守護執行緒;

 

FinalReference簡介

Finalizer繼承FinalReference類,FinalReference繼承Reference類,物件最終會被封裝為Finalizer物件,如果去檢視原始碼會發現Finalizer的構造方法是不對外暴漏,所以我們無法自己建立Finalizer物件,FinalReference是由jvm自動封裝;

 

什麼樣的物件會被封裝為Finalizer物件?

這裡重點要說明一下什麼樣的類才能被封裝為Finalizer:

1.      當前類或其父類含有一個引數為空,返回值為void,名為finalize的方法;

2.      這個finalize方法體不能為空;

滿足以上條件的類稱之為f

f類的物件是如何被封裝為Finalizer物件的?

         Finalizer類的兩個關鍵的方法

         //私有的構造方法

private Finalizer(Objectfinalizee) {

   //被封裝的物件和全域性的f-queue

        super(finalizee,queue);

        //呼叫add方法,將物件入隊

        add();

}

 

    //靜態的register方法,注意它的註釋“被vm呼叫”,所以jvm是通過呼叫這個方法將物件封裝為Finalizer物件的;

   /* Invoked by VM */

   staticvoid register(Objectfinalizee) {

        new Finalizer(finalizee);

    }

  

那麼jvm又是在何時呼叫register方法的呢?

取決於-XX:+RegisterFinalizersAtInit這個引數,預設為true,在呼叫建構函式返回之前呼叫Finalizer.register方法,果通過-XX:-RegisterFinalizersAtInit關閉了該引數,那將在物件空間分配好之後就將這個物件註冊進去。所以我們建立一個重寫了finalize方法的類

 

本文Finalizer物件註冊內容參考的笨神的一篇文章,要了解更詳細的內容請,請看這裡:

http://lovestblog.cn/blog/2015/07/09/final-reference/

 

Finalizer解析

重要的成員變數:

 

 

//它是unfialized佇列的隊頭,這個佇列裡面存了Finalizer物件,這裡面的Finalizer物件引用的f類都還沒有執行finalized方法unfializednextprev三個成員變數組成一個雙向連結串列的資料結構,unfialized永遠指向這個連結串列的頭,Finalizer物件建立的時候會加入到此佇列頭部,它是靜態全域性唯一的,所以在這個連結串列裡面的物件都是無法被回收的;在執行f類的finalizer之前,會將引用它的Finalizer物件從unfinalized佇列裡移除;這個佇列的作用是儲存全部的只存在FinalizerReference引用、且沒有執行過finalize方法的f類的Finalizer物件,防止finalizer物件在其引用的物件之前被gc回收掉

privatestaticFinalizerunfinalized=null;

//佇列連結串列中後一個物件和前一個物件的引用,由此可見它是一個雙向連結串列

private Finalizernext =null,prev =null;

 

//操作unfinalized佇列的全域性鎖,入隊出隊操作都需要加鎖

privatestaticfinal Objectlock =new Object();

 

//ReferenceQueue佇列,傳說中的f-queue佇列,這個佇列是全域性唯一的,當gc執行緒發現f類物件除了Finalizer引用外,沒有強引用了,就會把它放入到pending佇列, HanderReference執行緒在pending佇列取到FinalReference物件的時候,會將把他們都放到這個f-queue佇列裡面,然後Finalizer執行緒就可以去這個佇列裡取出Finalizer物件,在將其移出unfinized佇列,最後呼叫f類的finalizer方法;

privatestaticReferenceQueue<Object>queue =newReferenceQueue<>();

 

 

根據上面的成員變數可以看到Finalizer有兩個佇列,一個是unfialized,一個是f-queue佇列;

 

從上面的介紹我們可以知道,f類物件都有一個返回值為void、引數為空且方法體非空finalize()方法,在f類物件建立的時候,jvm同時也會建立一個Finalizer物件引用這個f類物件, Finalizer物件建立時就被加入到了unfialized裡面;

 

Finalizer執行緒

 

privatestatic class FinalizerThread extends Thread {

       private volatile boolean running;

       FinalizerThread(ThreadGroup g) {

           super(g, "Finalizer");

       }

       public void run() {

           //如果run方法已經在執行了,直接退出;running值會在後面標記為true

           if (running)

                return;

 

           // Finalizer thread starts before System.initializeSystemClass

           // is called.  Wait untilJavaLangAccess is available

           //等待jvm初始化完成後才繼續執行

while(!VM.isBooted()) {

                // delay until VM completesinitialization

                try {

                    VM.awaitBooted();

                } catch (InterruptedExceptionx) {

                    // ignore and continue

                }

           }

           final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();

           running = true;

           for (;;) {

                try {

                  //將物件從ReferenceQueue中移除,

                    Finalizer f =(Finalizer)queue.remove();

                  //通過runFinalizer呼叫finalizer方法

                    f.runFinalizer(jla);

                } catch (InterruptedExceptionx) {

                    // ignore and continue

                }

           }

       }

    }

    //靜態程式碼塊中初始化啟動FinalizerThread執行緒,注意它的優先順序是Thread.MAX_PRIORITY– 2 = 8,很多文章說它的優先順序很低,這是不對的,java裡面執行緒優先順序預設都是5

   static {

       ThreadGroup tg = Thread.currentThread().getThreadGroup();

       for (ThreadGroup tgn = tg;

            tgn != null;

            tg = tgn, tgn = tg.getParent());

       Thread finalizer = new FinalizerThread(tg);

       finalizer.setPriority(Thread.MAX_PRIORITY- 2);

       finalizer.setDaemon(true);

       finalizer.start();

    }

 

建立第二個Finalizer執行緒

預設Finalizer執行緒只有一個,在Finalizer裡面有這樣的一個方法,可以第二個名為“Secondary finalizer ”的finalizer執行緒來執行回收操作;

 

/* Create a privileged secondaryfinalizer thread in the system thread

       group for the given Runnable, and waitfor it to complete.

 

       This method is used by bothrunFinalization and runFinalizersOnExit.

       The former method invokes all pendingfinalizers, while the latter

       invokes all uninvoked finalizers ifon-exit finalization has been

       enabled.

 

       These two methods could have beenimplemented by offloading their work

       to the regular finalizer thread andwaiting for that thread to finish.

       The advantage of creating a freshthread, however, is that it insulates

       invokers of these methods from a stalledor deadlocked finalizer thread.

     */

   privatestaticvoid forkSecondaryFinalizer(final Runnable proc) {

        AccessController.doPrivileged(

            new PrivilegedAction<Void>() {

                public Void run() {

                ThreadGroup tg = Thread.currentThread().getThreadGroup();

                for (ThreadGrouptgn =tg;

                     tgn !=null;

                     tg =tgn,tgn=tg.getParent());

                Thread sft =new Thread(tg,proc,"Secondary finalizer");

                sft.start();

                try {

                    sft.join();

                } catch (InterruptedExceptionx) {

                    /* Ignore */

                }

                returnnull;

                }});

   }

 

   /* Called by Runtime.runFinalization() */

   staticvoid runFinalization() {

        if (!VM.isBooted()) {

            return;

        }

 

        forkSecondaryFinalizer(new Runnable() {

            privatevolatilebooleanrunning;

            publicvoid run() {

                if (running)

                    return;

                final JavaLangAccessjla = SharedSecrets.getJavaLangAccess();

                running =true;

                for (;;) {

                    Finalizer f = (Finalizer)queue.poll();

                    if (f ==null)break;

                    f.runFinalizer(jla);

                }

            }

        });

}

runFinalization可以建立第二個Finalizer執行緒,它無法直接呼叫,可以通過以下方法呼叫

System.runFinalization();

Runtime.getRuntime().runFinalization();

如下圖所示




finalizer導致的問題

 

問題一:Finalizer執行緒是一個單執行緒來處理F-queue,雖然可以再啟動第二個,但是也是兩個執行緒而已,如果系統中有很多執行緒爭用cpu,在系統壓力比較大的情況下,Finalizer執行緒獲取到cpu時間片的時間是不確定的,在其獲取到時間片之前,應該被回收的Finalizer物件一直在佇列中積累,佔用大量記憶體,經過n次gc後,仍然沒有機會被釋放掉,這些物件都進入到老年代,導致old剩餘空間變小,從而使fullgc會更加頻繁,如果Finalizer物件積壓嚴重的甚至會導致oom;

問題二:如果Finalizer物件生產的速度比Finalizer執行緒處理的速度要快,也會導致F-queue佇列裡面的Finalizer物件積壓,這些物件一直佔用jvm的記憶體,直到oom;如果執行某個f類的finalizer方法執行非常耗時,或這個方法裡面的操作被鎖阻塞了Finalizer執行緒,那麼就會導致佇列裡面其它的Finalizer物件一直在等待佇列裡面無法被回收釋放空間,最終導致oom; 

問題三:Reference物件是在gc的時候來處理的,如果沒有觸發GC就沒有機會觸發Reference引用的處理操作,那麼應該被回收的FinalReference物件就一直在unfinalized佇列裡,無法被回收,導致被它引用的物件也無法回收,然後又導致被引用物件佔用的資源也不會釋放,最終可能會導致native資源耗盡;

問題四:可能導致資源洩露,例如當jvm退出時,很可能unfinalizer佇列裡的物件沒有被處理完就退出了;

問題五:物件有可能在執行過finalize方法後,又被強引用引用到了,於是物件就復活了;

 

所以釋放資源一定要手動去釋放,如果忘記釋放,依靠finalizer的機制是不靠譜的,很可能會導致一些嚴重的記憶體問題或native資源洩露問題,如果一定要用,必須保證呼叫finalize方法能夠快速執行完成;


另外java裡面還有一個sun.misc.Cleaner類,它繼承自PhantomReference,作用同Finalize一樣,它的清理工作是在ReferenceHandel執行緒裡面完成的,只是少了Finalizer執行緒處理這一步,Finalize存在的問題,它基本都有,如果clean方法使用不當,阻塞ReferenceHander執行緒,會導致比finalizer執行緒更加嚴重的問題;

在java裡面DirectByteBuffer這個類就是使用Cleaner清理的,有空再寫一篇文章講解一下DirectByteBuffer的坑!