java.lang.ref
該包下提供了Reference相關(guān)的類弃舒,包括基類Reference,三個子類WeakReference状原、SoftReference和PhantomReference聋呢,以及一個能和它們配合使用的類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并不是很可靠卦睹。我們不能過度依賴這個方法畦戒,其實使用PhantomReference和ReferenceQueue也能達到對應(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)存泄漏的一個庫三幻,這其中的原理就是使用WeakReference和ReferenceQueue完成的。這里只描述一下呐能,就不貼代碼了念搬。
- 在應(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 中的引用類型初探