理解Reference

java.lang.ref

該包下提供了Reference相關(guān)的類,包括基類Reference,三個(gè)子類WeakReferenceSoftReferencePhantomReference,以及一個(gè)能和它們配合使用的類ReferenceQueue。通過使用這些類,開發(fā)者可以通過包裝目標(biāo)對象,創(chuàng)建指向目標(biāo)對象的不同的引用類型。使用這些引用類,并不會阻礙JVM對目標(biāo)對象的回收。并且,如果和ReferenceQueue配合使用,在目標(biāo)對象的可達(dá)性發(fā)生變化時(shí),我們還能得到JVM的通知(確切來說是通過查詢與之關(guān)聯(lián)的引用隊(duì)列感知到這種變化),這可能對我們監(jiān)控目標(biāo)對象的生命周期很有幫助。通過使用這種方式,開發(fā)者和JVM的垃圾回收器能夠有一定程度的交互。

目標(biāo)對象可達(dá)性的定義

在JVM中,通過可達(dá)性可以判斷一個(gè)目標(biāo)對象是否存活從而進(jìn)行垃圾回收。JVM會從GC Roots(如線程局部變量、類靜態(tài)變量等)開始遍歷,構(gòu)建一顆引用樹,如果不存在至目標(biāo)對象的引用路徑,目標(biāo)對象將標(biāo)記為不可達(dá),并在未來進(jìn)行回收。目標(biāo)對象某些時(shí)刻可能同時(shí)存在多條引用路徑。對象的可達(dá)性主要有:

  • 強(qiáng)可達(dá):目標(biāo)對象至少存在一條引用路徑,該引用路徑中不包含(不經(jīng)過)任何的Reference類。
  • 軟可達(dá):目標(biāo)對象非強(qiáng)可達(dá),且至少存在這樣一條引用路徑,該路徑中包含(經(jīng)過)的第一個(gè)Reference類為SoftReference;(分為兩種情況:1.目標(biāo)對象為SoftReference中的根;2.目標(biāo)對象在SoftReference中的根對象的某條引用路徑上)
  • 弱可達(dá):目標(biāo)對象非強(qiáng)可達(dá)和軟可達(dá),且至少存在這樣一條引用路徑,該路徑中包含(經(jīng)過)的第一個(gè)Reference類為WeakReference;
  • 虛可達(dá):目標(biāo)對象非強(qiáng)可達(dá)、軟可達(dá)和弱可達(dá),且至少存在這樣一條引用路徑,該路徑中包含(經(jīng)過)的第一個(gè)Reference類為PhantomReference,且該對象已經(jīng)執(zhí)行過finalize方法;
  • 不可達(dá):不存在任何至目標(biāo)對象的引用路徑。

對象的可達(dá)性是互斥的,從上至下可達(dá)性遞減;對象如果同時(shí)存在多條引用路徑,那么可達(dá)性由最強(qiáng)的路徑?jīng)Q定;

finalize和對象的狀態(tài)

我們都知道,Object類中有個(gè)finalize方法;GC Collector中存在這樣一個(gè)隊(duì)列F-QUEUE,在GC首次標(biāo)記一個(gè)對象為不可達(dá)時(shí),如果目標(biāo)對象重寫了finalize方法,會將該對象添加至這個(gè)隊(duì)列中,且狀態(tài)變?yōu)?em>finalizable。同時(shí),也存在這樣一個(gè)后臺線程,姑且叫做finalizer-handler,它負(fù)責(zé)不斷的從前面的隊(duì)列中取出對象并執(zhí)行finalize方法。一個(gè)對象如果執(zhí)行過finalize方法,狀態(tài)就是finalized;之后,如果對象的可達(dá)性不再發(fā)生變化,那么該對象就會被回收了。為什么這樣說呢?因?yàn)樵?em>finalize方法中,我們可以改變該對象的可達(dá)性,比如重新引用該對象,通過這種方式,我們拯救了一個(gè)即將被回收的對象,這種情況也叫做對象重生。如下面的代碼所示:

public class Reborn{
    
    static Reborn sNewLife;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        sNewLife = this;
    }
}

那提供finalize方法的意義何在呢?因?yàn)镚C只負(fù)責(zé)內(nèi)存相關(guān)的回收工作,其他資源需要開發(fā)者自己釋放,如數(shù)據(jù)庫連接、文件句柄等。因此,我們可以根據(jù)需要重寫該方法,在對象被回收之前,做一些最后的清理工作。但是在使用時(shí)需注意:

  • 如果沒有重寫finalize方法,或者重寫了但只采用默認(rèn)實(shí)現(xiàn),那么GC不會將目標(biāo)對象加入F-QUEUE,而是直接回收對象。
  • GC會記錄相應(yīng)的狀態(tài),對象的finalize方法在對象生命周期過程中只會被執(zhí)行一次。因此如果對象在finalize方法中重生了,下一次再進(jìn)入回收階段時(shí),不會再執(zhí)行該方法。應(yīng)該盡量避免對象的再生,如果非要再生,請不要直接使用當(dāng)前對象,而是基于當(dāng)前對象重新構(gòu)建一個(gè)新的對象。

基于以上的原因,其實(shí)finalize并不是很可靠。我們不能過度依賴這個(gè)方法,其實(shí)使用PhantomReferenceReferenceQueue也能達(dá)到對應(yīng)的效果且更穩(wěn)定,這個(gè)后面再說。

介紹完finalize,我們再來說說對象的狀態(tài)劃分。在虛擬機(jī)中,對象的狀態(tài)可以總結(jié)為以下幾個(gè)階段(細(xì)分的話還有其他狀態(tài),但跟Reference相關(guān)的主要是以下這幾個(gè)):

  • Reachable:可達(dá)的,這里的可達(dá)指強(qiáng)可達(dá);一般而言,新創(chuàng)建的對象都處于這個(gè)狀態(tài);
  • Finalizable:即將執(zhí)行對象的finalize方法,F-QUEUE中的對象都是這個(gè)狀態(tài);
  • Finalized:已經(jīng)執(zhí)行過對象的finalize方法,此時(shí)對象可能是可達(dá)的或者是等待回收的狀態(tài)的,因?yàn)?em>對象可能重生;
  • Reclaimable:可回收的;處于該狀態(tài)的對象是Finalized的且沒有其他強(qiáng)引用的。
  • Reclaimed:完成內(nèi)存回收。

這些狀態(tài)之間有些是互斥的,有些是能夠并存的!比如一個(gè)再生的對象應(yīng)該是Reachable且Finalized的。而一個(gè)虛可達(dá)的對象是Finalized且Reclaimable的,只要清空引用就能真正被回收。

Reference類如何工作

Reference類的三個(gè)子類可以單獨(dú)使用,也可以和ReferenceQueue配合使用,在目標(biāo)對象的可達(dá)性發(fā)生變化時(shí),如果提供有ReferenceQueue,那么會將該Reference對象加入到隊(duì)列中。開發(fā)者通過ReferenceQueue#poll或是ReferenceQueue#remove方法查看隊(duì)列是否包含對應(yīng)的Reference對象,從而可以判斷目標(biāo)對象的可達(dá)性是否發(fā)生了變化,這方便了監(jiān)控或是進(jìn)行其他與對象生命周期相關(guān)的處理邏輯。先來看看WeakReference的工作過程:

  • 直接將對應(yīng)的Reference對象設(shè)置為null,不會觸發(fā)下面的處理過程
  • 未被處理時(shí),WeakReference#get方法可以返回目標(biāo)對象的引用
  • GC時(shí),一旦檢測到目標(biāo)對象僅為弱可達(dá),無論當(dāng)時(shí)的內(nèi)存情況如何,會進(jìn)一步處理WeakReference
  • 具體的,GC會清除掉所有WeakReference中對目標(biāo)對象的引用,即將referent字段置為null,這樣會導(dǎo)致WeakReference#get方法將返回null
  • 如果目標(biāo)對象需要執(zhí)行finalize方法(有實(shí)現(xiàn)且未執(zhí)行過),則加入F-QUEUE,目標(biāo)對象轉(zhuǎn)到finalizable狀態(tài)
  • 與此同時(shí)或之后某個(gè)時(shí)間,將WeakReference對象添加到對應(yīng)的ReferenceQueue隊(duì)列中(如果存在)
  • 當(dāng)我們從ReferenceQueue中查詢到對應(yīng)的WeakReference對象時(shí),并不知道目標(biāo)對象的命運(yùn)到底是如何或會如何!這個(gè)時(shí)候有可能并沒有執(zhí)行finalize方法,也可能執(zhí)行過了!我們只知道目標(biāo)對象曾經(jīng)是finalizable的,可能執(zhí)行完finalize方法之后,目標(biāo)對象又重生了**
  • 處理目標(biāo)對象時(shí),會級聯(lián)處理通過目標(biāo)對象到達(dá)的其他弱可達(dá)的對象
@Test(timeout = 10000)
public void weak_reference() throws InterruptedException {

    A a = new A();
    ReferenceQueue<A> queue = new ReferenceQueue<>();//關(guān)聯(lián)的隊(duì)列
    WeakReference<A> weakReferenceA = new WeakReference<>(a, queue);
    a = null;//目標(biāo)對象a只存在弱引用,為弱可達(dá)
    Runtime.getRuntime().gc(); //更容易觸發(fā)gc
    Thread.sleep(2000);

    assertTrue(A.sA != null);//對象重生了
    assertTrue(weakReferenceA.get() == null);//引用被GC Clear掉了

    //check queue
    while (true){
        Reference<A> item = (Reference<A>) queue.poll();
        if (item != null){
            assertTrue(weakReferenceA == item);//被添加到隊(duì)列中了
            break;
        }
    }
}

GC時(shí),目標(biāo)對象僅存在弱引用,接著弱引用被清除,并被添加到引用隊(duì)列中。雖然對象a通過finalize方法完成了再生,但不妨礙它被清除且添加至引用隊(duì)列中。

對于SoftReference來說,基本類似于WeakReference的表現(xiàn)。只有一點(diǎn)需要注意,GC在內(nèi)存不足時(shí)才會處理軟引用可達(dá)的對象,而WeakReference弱可達(dá)是一旦GC觸發(fā)就會處理
而對于PhantomReference就跟前兩者不太一樣,具體說明如下:

  • 直接將對應(yīng)的Reference對象設(shè)置為null,不會觸發(fā)下面的處理過程
  • 無論是否已經(jīng)被處理,PhantomReference#get方法始終返回null
  • GC時(shí),一旦檢測到目標(biāo)對象僅為虛可達(dá),無論當(dāng)時(shí)的內(nèi)存情況如何,會進(jìn)一步處理PhantomReference
  • GC不會清除掉PhantomReference中對目標(biāo)對象的引用,即不會將對象中的referent字段置為null,需要我們手動調(diào)用clear方法進(jìn)行清除
  • 如果目標(biāo)對象需要執(zhí)行finalize方法(有實(shí)現(xiàn)且未執(zhí)行過),則加入F-QUEUE,目標(biāo)對象轉(zhuǎn)到finalizable狀態(tài)
  • PhantomReference對象不會立刻被添加至對應(yīng)的ReferenceQueue隊(duì)列中,需要確保目標(biāo)對象執(zhí)行完成finalize方法,且不會重生;即:當(dāng)我們在ReferenceQueue中檢測到PhantomReference對象時(shí),它所包裝的目標(biāo)對象肯定是Finalized,且僅僅存在虛引用的,也就是說此時(shí)目標(biāo)對象的狀態(tài)為Reclaimable
  • 因?yàn)樘撘玫膔eferent不會被gc主動clear,因此需要我們手動調(diào)用clear方法,或者將對應(yīng)PhantomReference變?yōu)椴豢蛇_(dá),否則目標(biāo)對象也不會被執(zhí)行到最后的內(nèi)存回收階段,而僅僅是保持在可回收狀態(tài)
  • 處理目標(biāo)對象時(shí),會級聯(lián)處理通過目標(biāo)對象到達(dá)的其他虛可達(dá)的對象
@Test(timeout = 20000)
public void phantom_reference2() throws InterruptedException, NoSuchFieldException, IllegalAccessException {

    A a = new A();
    ReferenceQueue<A> queue = new ReferenceQueue<>();
    PhantomReference<A> phantomReferenceA = new PhantomReference<>(a, queue);
    assertTrue(phantomReferenceA.get() == null); //get方法始終返回null

    a = null; //對象僅虛可達(dá)
    System.gc();
    Thread.sleep(2000);

    assertTrue(A.sA != null);//對象在finalize中重生,因此不會加入引用隊(duì)列中
   
     //如果下面的代碼注釋掉了,測試用例會因?yàn)閠imeout而執(zhí)行失敗
    //A.sA = null;
    //System.gc();
    //Thread.sleep(2000);

    //check queue
    while (true){
        Reference<A> item = (Reference<A>) queue.poll();
        if (item != null){
            Field field = Reference.class.getDeclaredField("referent");
            field.setAccessible(true);
            Object object = field.get(item);
            
            //區(qū)別于WeakReference和SoftReference,GC不會幫PhantomReference自動清理
            assertTrue(object != null);
            
            //需要手動clear掉
            item.clear();

            break;
        }
    }
}

當(dāng)我們將中間的一段代碼注釋掉運(yùn)行時(shí),測試用例會因?yàn)槌瑫r(shí)運(yùn)行失敗,因?yàn)樵谝藐?duì)列中無法獲取對應(yīng)的PhantomReference導(dǎo)致死循環(huán),因?yàn)镽eference對象不滿足加入到隊(duì)列中的條件finalized且僅虛可達(dá),而當(dāng)我們不注釋這段代碼時(shí),運(yùn)行正常。需要注意的是,在代碼中,我們還通過反射方式去獲取對象中的referent字段,發(fā)現(xiàn)是存在值得且可用的,說明虛擬機(jī)并沒有自動替我們清理掉這個(gè)字段,這一點(diǎn)不同于上面的兩個(gè)Reference類型。

Reference和ReferenceQueue的應(yīng)用案例

通過上面的說明,我們已經(jīng)了解了Reference和ReferenceQueue的大概,下面來看它們配合使用的一個(gè)例子;LeakCanary,想必大家都很熟悉了,它是開發(fā)階段用來檢測內(nèi)存泄漏的一個(gè)庫,這其中的原理就是使用WeakReferenceReferenceQueue完成的。這里只描述一下,就不貼代碼了。

  • 在應(yīng)用的Application類中注冊一個(gè)ActivityLifecycleCallbacks回調(diào),重寫onActivityDestroyed(Activity activity)方法
  • 在退出Activity的時(shí)候,該回調(diào)中的destroy方法觸發(fā),創(chuàng)建一個(gè)WeakReference對象包裝這個(gè)銷毀的activity目標(biāo)對象,并指定一個(gè)ReferenceQueue
  • 觸發(fā)GC并監(jiān)控ReferenceQueue的變化。因?yàn)閍ctivity對象即將被銷毀,因此未來某個(gè)時(shí)刻應(yīng)該僅僅存在該activity的弱引用并在GC時(shí)得到處理,對應(yīng)的WeakReference對象被添加至ReferenceQueue中。如果一直檢測不到該WeakReference對象被添加至隊(duì)列中,說明肯定存在其他的引用路徑,也就代表了可能存在內(nèi)存泄漏問題
  • 通過android.os.Debug#dumpHprofData方法dump此時(shí)的java heap至一個(gè)文件中,在后臺通過工具分析該heap profile,找出目標(biāo)activity的引用路徑,發(fā)送狀態(tài)欄通知告知開發(fā)者

大致的流程就是這樣,除了最后一步的dump,前面的都比較簡單。說到這里,又不得不提以下Android SDK中的StrictMode類。該類可以幫助開發(fā)者在開發(fā)階段發(fā)現(xiàn)一些問題。通過該類也能檢測到Activity的泄露,但相比leakcanary,它僅能通過打印日志或是拋出異常通知開發(fā)者可能發(fā)生leak,但不能給出引用路徑,還是需要開發(fā)者自己去dump heap,自己去分析。另外還有一點(diǎn),它檢測leak的方式是區(qū)別于leakcanary的。當(dāng)開啟leak檢測的時(shí)候,StrictMode類中會記錄所有activity類的instance數(shù)量,通過一個(gè)靜態(tài)的hashmap字段保存。而在創(chuàng)建和銷毀activity的地方會更新activity類對應(yīng)的instance數(shù)量。如下:

public class ActivityThread{

    ...

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ...
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        ...
    }

    private ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
                                                        int configChanges, boolean getNonConfigInstance) {
        ...
        mActivities.remove(token);
        StrictMode.decrementExpectedActivityCount(activityClass);
        ...
    }

}

在StrictMode.decrementExpectedActivityCount方法中,會觸發(fā)GC,然后檢測預(yù)期的activity的實(shí)例數(shù)量和實(shí)際的實(shí)例數(shù)量是否一致來判斷是否發(fā)生leak,而實(shí)際的實(shí)例數(shù)量通過android.os.Debug#countInstancesOfClass方法可以獲取。除了檢測activity泄露,StrictMode在開發(fā)階段還能做更多事情,如檢測類的實(shí)例數(shù)量是否超出限制、SqliteObjectLeaks、RegistrationLeaks等,當(dāng)然這些和Reference扯不上關(guān)系,就不談了,有興趣可以自己去看代碼。

參考

Reachability
深入理解java的finalize
深入理解ReferenceQueue GC finalize Reference
Android 中的引用類型初探

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

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

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