理解Reference

java.lang.ref

該包下提供了Reference相關(guān)的類弃舒,包括基類Reference,三個子類WeakReference状原、SoftReferencePhantomReference聋呢,以及一個能和它們配合使用的類ReferenceQueue。通過使用這些類颠区,開發(fā)者可以通過包裝目標(biāo)對象削锰,創(chuàng)建指向目標(biāo)對象的不同的引用類型。使用這些引用類毕莱,并不會阻礙JVM對目標(biāo)對象的回收器贩。并且,如果和ReferenceQueue配合使用朋截,在目標(biāo)對象的可達性發(fā)生變化時蛹稍,我們還能得到JVM的通知(確切來說是通過查詢與之關(guān)聯(lián)的引用隊列感知到這種變化),這可能對我們監(jiān)控目標(biāo)對象的生命周期很有幫助部服。通過使用這種方式唆姐,開發(fā)者和JVM的垃圾回收器能夠有一定程度的交互。

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

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

  • 強可達:目標(biāo)對象至少存在一條引用路徑车吹,該引用路徑中不包含(不經(jīng)過)任何的Reference類筹裕。
  • 軟可達:目標(biāo)對象非強可達,且至少存在這樣一條引用路徑窄驹,該路徑中包含(經(jīng)過)的第一個Reference類為SoftReference朝卒;(分為兩種情況:1.目標(biāo)對象為SoftReference中的根;2.目標(biāo)對象在SoftReference中的根對象的某條引用路徑上)
  • 弱可達:目標(biāo)對象非強可達和軟可達乐埠,且至少存在這樣一條引用路徑抗斤,該路徑中包含(經(jīng)過)的第一個Reference類為WeakReference
  • 虛可達:目標(biāo)對象非強可達丈咐、軟可達和弱可達瑞眼,且至少存在這樣一條引用路徑,該路徑中包含(經(jīng)過)的第一個Reference類為PhantomReference棵逊,且該對象已經(jīng)執(zhí)行過finalize方法伤疙;
  • 不可達:不存在任何至目標(biāo)對象的引用路徑。

對象的可達性是互斥的辆影,從上至下可達性遞減徒像;對象如果同時存在多條引用路徑,那么可達性由最強的路徑?jīng)Q定蛙讥;

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

我們都知道厨姚,Object類中有個finalize方法;GC Collector中存在這樣一個隊列F-QUEUE键菱,在GC首次標(biāo)記一個對象為不可達時谬墙,如果目標(biāo)對象重寫了finalize方法,會將該對象添加至這個隊列中经备,且狀態(tài)變?yōu)?em>finalizable拭抬。同時,也存在這樣一個后臺線程侵蒙,姑且叫做finalizer-handler造虎,它負責(zé)不斷的從前面的隊列中取出對象并執(zhí)行finalize方法。一個對象如果執(zhí)行過finalize方法纷闺,狀態(tài)就是finalized算凿;之后,如果對象的可達性不再發(fā)生變化犁功,那么該對象就會被回收了涩赢。為什么這樣說呢湃窍?因為在finalize方法中,我們可以改變該對象的可達性,比如重新引用該對象和泌,通過這種方式,我們拯救了一個即將被回收的對象,這種情況也叫做對象重生。如下面的代碼所示:

public class Reborn{
    
    static Reborn sNewLife;

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

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

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

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

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

  • Reachable:可達的,這里的可達指強可達返敬;一般而言遂庄,新創(chuàng)建的對象都處于這個狀態(tài);
  • Finalizable:即將執(zhí)行對象的finalize方法救赐,F-QUEUE中的對象都是這個狀態(tài)涧团;
  • Finalized:已經(jīng)執(zhí)行過對象的finalize方法只磷,此時對象可能是可達的或者是等待回收的狀態(tài)的经磅,因為對象可能重生泌绣;
  • Reclaimable:可回收的;處于該狀態(tài)的對象是Finalized的且沒有其他強引用的预厌。
  • Reclaimed:完成內(nèi)存回收阿迈。

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

Reference類如何工作

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

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

    A a = new A();
    ReferenceQueue<A> queue = new ReferenceQueue<>();//關(guān)聯(lián)的隊列
    WeakReference<A> weakReferenceA = new WeakReference<>(a, queue);
    a = null;//目標(biāo)對象a只存在弱引用烙常,為弱可達
    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);//被添加到隊列中了
            break;
        }
    }
}

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

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

  • 直接將對應(yīng)的Reference對象設(shè)置為null固耘,不會觸發(fā)下面的處理過程
  • 無論是否已經(jīng)被處理题篷,PhantomReference#get方法始終返回null
  • GC時,一旦檢測到目標(biāo)對象僅為虛可達厅目,無論當(dāng)時的內(nèi)存情況如何番枚,會進一步處理PhantomReference
  • GC不會清除掉PhantomReference中對目標(biāo)對象的引用,即不會將對象中的referent字段置為null璧瞬,需要我們手動調(diào)用clear方法進行清除
  • 如果目標(biāo)對象需要執(zhí)行finalize方法(有實現(xiàn)且未執(zhí)行過)户辫,則加入F-QUEUE,目標(biāo)對象轉(zhuǎn)到finalizable狀態(tài)
  • PhantomReference對象不會立刻被添加至對應(yīng)的ReferenceQueue隊列中嗤锉,需要確保目標(biāo)對象執(zhí)行完成finalize方法渔欢,且不會重生;即:當(dāng)我們在ReferenceQueue中檢測到PhantomReference對象時瘟忱,它所包裝的目標(biāo)對象肯定是Finalized奥额,且僅僅存在虛引用的,也就是說此時目標(biāo)對象的狀態(tài)為Reclaimable
  • 因為虛引用的referent不會被gc主動clear访诱,因此需要我們手動調(diào)用clear方法垫挨,或者將對應(yīng)PhantomReference變?yōu)椴豢蛇_,否則目標(biāo)對象也不會被執(zhí)行到最后的內(nèi)存回收階段触菜,而僅僅是保持在可回收狀態(tài)
  • 處理目標(biāo)對象時九榔,會級聯(lián)處理通過目標(biāo)對象到達的其他虛可達的對象
@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; //對象僅虛可達
    System.gc();
    Thread.sleep(2000);

    assertTrue(A.sA != null);//對象在finalize中重生,因此不會加入引用隊列中
   
     //如果下面的代碼注釋掉了涡相,測試用例會因為timeout而執(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īng)的PhantomReference導(dǎo)致死循環(huán)切威,因為Reference對象不滿足加入到隊列中的條件finalized且僅虛可達,而當(dāng)我們不注釋這段代碼時丙号,運行正常先朦。需要注意的是缰冤,在代碼中,我們還通過反射方式去獲取對象中的referent字段喳魏,發(fā)現(xiàn)是存在值得且可用的棉浸,說明虛擬機并沒有自動替我們清理掉這個字段,這一點不同于上面的兩個Reference類型截酷。

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

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

  • 在應(yīng)用的Application類中注冊一個ActivityLifecycleCallbacks回調(diào),重寫onActivityDestroyed(Activity activity)方法
  • 在退出Activity的時候摆出,該回調(diào)中的destroy方法觸發(fā)朗徊,創(chuàng)建一個WeakReference對象包裝這個銷毀的activity目標(biāo)對象,并指定一個ReferenceQueue
  • 觸發(fā)GC并監(jiān)控ReferenceQueue的變化偎漫。因為activity對象即將被銷毀爷恳,因此未來某個時刻應(yīng)該僅僅存在該activity的弱引用并在GC時得到處理,對應(yīng)的WeakReference對象被添加至ReferenceQueue中象踊。如果一直檢測不到該WeakReference對象被添加至隊列中温亲,說明肯定存在其他的引用路徑,也就代表了可能存在內(nèi)存泄漏問題
  • 通過android.os.Debug#dumpHprofData方法dump此時的java heap至一個文件中杯矩,在后臺通過工具分析該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坛芽,自己去分析留储。另外還有一點,它檢測leak的方式是區(qū)別于leakcanary的咙轩。當(dāng)開啟leak檢測的時候获讳,StrictMode類中會記錄所有activity類的instance數(shù)量,通過一個靜態(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ù)量是否一致來判斷是否發(fā)生leak帅矗,而實際的實例數(shù)量通過android.os.Debug#countInstancesOfClass方法可以獲取。除了檢測activity泄露煞烫,StrictMode在開發(fā)階段還能做更多事情浑此,如檢測類的實例數(shù)量是否超出限制、SqliteObjectLeaks滞详、RegistrationLeaks等凛俱,當(dāng)然這些和Reference扯不上關(guān)系,就不談了料饥,有興趣可以自己去看代碼蒲犬。

參考

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市岸啡,隨后出現(xiàn)的幾起案子原叮,更是在濱河造成了極大的恐慌,老刑警劉巖凰狞,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件篇裁,死亡現(xiàn)場離奇詭異,居然都是意外死亡赡若,警方通過查閱死者的電腦和手機达布,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逾冬,“玉大人黍聂,你說我怎么就攤上這事∩砟澹” “怎么了产还?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長嘀趟。 經(jīng)常有香客問我脐区,道長,這世上最難降的妖魔是什么她按? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任牛隅,我火速辦了婚禮炕柔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘媒佣。我一直安慰自己匕累,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布默伍。 她就那樣靜靜地躺著欢嘿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪也糊。 梳的紋絲不亂的頭發(fā)上炼蹦,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音显设,去河邊找鬼框弛。 笑死,一個胖子當(dāng)著我的面吹牛捕捂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播斗搞,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼指攒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了僻焚?” 一聲冷哼從身側(cè)響起允悦,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎虑啤,沒想到半個月后隙弛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡狞山,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年全闷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片萍启。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡总珠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出勘纯,到底是詐尸還是另有隱情局服,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布驳遵,位于F島的核電站淫奔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏堤结。R本人自食惡果不足惜唆迁,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一佳鳖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧媒惕,春花似錦系吩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至肴盏,卻和暖如春科盛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背菜皂。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工贞绵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人恍飘。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓榨崩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親章母。 傳聞我的和親對象是個殘疾皇子母蛛,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容