4、常見內(nèi)存泄漏
這是一個(gè)老生常談的一個(gè)問題了追城,但我還是先對(duì)Java中的內(nèi)存泄漏做一個(gè)定義:
Java中的內(nèi)存泄漏就是存在一些被分配的對(duì)象范咨,這些對(duì)象有下面兩個(gè)特點(diǎn)故觅,首先,這些對(duì)象是可達(dá)的湖蜕,即在有向圖中逻卖,存在通路可以與其相連;其次昭抒,這些對(duì)象是無用的评也,即程序以后不會(huì)再使用這些對(duì)象。如果對(duì)象滿足這兩個(gè)條件灭返,這些對(duì)象就可以判定為Java中的內(nèi)存泄漏盗迟,這些對(duì)象不會(huì)被GC所回收,然而它卻占用內(nèi)存熙含。
在C++中罚缕,內(nèi)存泄漏的范圍更大一些。有些對(duì)象被分配了內(nèi)存空間怎静,然后卻不可達(dá)邮弹,由于C++中沒有GC黔衡,這些內(nèi)存將永遠(yuǎn)收不回來。在Java中腌乡,這些不可達(dá)的對(duì)象都由GC負(fù)責(zé)回收盟劫,因此程序員不需要考慮這部分的內(nèi)存泄露。
對(duì)于Java中的內(nèi)存泄漏与纽,我總結(jié)為三點(diǎn):static侣签、線程 和 系統(tǒng)(外部)資源申請(qǐng)。
對(duì)于JVM內(nèi)部而言急迂,它的垃圾回收機(jī)制真的做的非常不錯(cuò)影所,我總覺得Java的出現(xiàn)是程序員的一次體力解放,大家再也不用去關(guān)心那個(gè)讓RD們睡不好覺的指針問題僚碎;而JVM內(nèi)部的內(nèi)存泄漏猴娩,追究起原因來,我覺得(瞎估的)90%是static靜態(tài)變量引起的听盖、10%是while(true)的線程造成的胀溺,大家想一下,我們平時(shí)發(fā)現(xiàn)的內(nèi)存泄漏皆看,是不是大都是注冊(cè)了監(jiān)聽沒釋放仓坞,或者是聲明了一個(gè)大對(duì)象的static為了方便傳遞數(shù)據(jù),結(jié)果忘了置空腰吟;對(duì)于static這樣的變量无埃,我們聲明時(shí)要非常小心,我的意見是毛雇,不是非常必要的情況下嫉称,能不用盡量不要用。
外部資源申請(qǐng)一般指的是打開一些文件灵疮、設(shè)備织阅、數(shù)據(jù)庫(kù)等,這些系統(tǒng)都會(huì)給我們提供一些開銷震捣,如果我們沒能及時(shí)關(guān)閉掉這些設(shè)備荔棉,則會(huì)造成不必要的內(nèi)存開銷。
Android上的內(nèi)存泄漏會(huì)更具體一些蒿赢,有些也會(huì)很隱蔽润樱,我們來具體分析一下,這也是每個(gè)Android程序員面試都會(huì)考到的一個(gè)題羡棵。
(1)activity泄漏
這是我們平時(shí)最關(guān)心的泄漏壹若,因?yàn)锳ctivity在Android的四大組件中持的有資源最多,一個(gè)Activity沒回收,會(huì)導(dǎo)致它里面的無數(shù)個(gè)View都無法被回收到店展。
<1>在需要Context的地方傳入Activity养篓,導(dǎo)致被靜態(tài)變量持有,這種情況大家應(yīng)該碰到的很多赂蕴。
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static synchronized AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
....
// 使用時(shí)
AppManager am = AppManager.getInstance(activity);
上面這種情況觉至,activity就被靜態(tài)變量instance持有了,除非appmanager這個(gè)單例釋放了睡腿。
這種情況有兩個(gè)解決辦法:
1、AppManager am = AppManager.getInstance(activity.getApplicationContext()); // 不建議
2峻贮、private AppManager(Context context) {
this.context = context.getApplicationContext();
}
建議使用第二種方法席怪,因?yàn)檫@是從根上進(jìn)行的解決,而第一種方法是需要調(diào)用者來保證解決纤控,這種方法很不靠譜挂捻,而且從嚴(yán)格意義上說,調(diào)用都使用的根本沒錯(cuò)船万,他傳進(jìn)去的Activity就是一個(gè)Context刻撒。
<2>靜態(tài)變量Activity
這是Activity使用的大忌。
class MyActivity extends Activity {
public static Activity mActivity;
protected void onCreate(...) {
....
mActivity = this耿导;
}
}
這種寫法一般都是程序員偷懶声怔,想通過static的屬性快速獲取一個(gè)Activity的實(shí)例,但這種情況下一般使用的人不知道什么時(shí)候去把mActivity置空舱呻,很容易泄漏醋火。
<3>Static view或static Drawable
class MyActivity extends Activity {
public static View mView;
public static Drawable mDrawable;
}
這個(gè)跟<2>有點(diǎn)相似,都是程序員偷懶干出來的事情箱吕,只要這個(gè)view是這個(gè)activity中創(chuàng)出來的或者用activit inflate進(jìn)來的芥驳,那View中的context就是Activity,所以view在茬高,Activity就不會(huì)被回收兆旬;Drawable這個(gè)實(shí)際上是Android 2.3及以前的一個(gè)Bug,我們看代碼:
//View.setBackground方法:
publicvoidsetBackgroundDrawable(Drawable background) {
....
background.setCallback(this);
.......
}
// Drawable中的setCallback方法
public final void setCallback(Callback cb) {
mCallback = cb;
}
<4>非static的內(nèi)部類
直接看代碼
非靜態(tài)內(nèi)部類
非靜態(tài)內(nèi)部類怎栽,只要不在賦值給靜態(tài)變量丽猬,在類的內(nèi)部使用起來還是非常方便的,因?yàn)樗梢灾苯诱{(diào)用外部類的變量和方法婚瓜。但如果我們?cè)陬惖耐獠咳ew 一個(gè)這樣的對(duì)象宝鼓,我們應(yīng)該怎么寫呢?
MainActivity.TestResource inner = (new MainActivity()).new TestResource();
從這里可以看出巴刻,一個(gè)非靜態(tài)的內(nèi)部類對(duì)象是必須依附一個(gè)外部類對(duì)象存在的愚铡,這個(gè)時(shí)候如果內(nèi)部類對(duì)象被靜態(tài)變量持有,或者被傳出去注冊(cè)在哪里,就會(huì)導(dǎo)致外部類沥寥,比如這里的Activity無法回收碍舍。
<5>匿名內(nèi)部類,我們最容易忽略的泄漏
Thread造成的匿名內(nèi)部類泄漏
再看一種:
AppManager.getInstance(mContext).registerStateChangedListener(new ? ? AppStateChangedListener() {
....
});
上面一種是線程造成的泄漏邑雅,線程不停片橡,資源不釋放;后一個(gè)是單例造成的淮野;這兩種匿名內(nèi)部類沒有變量持有捧书,基本是必泄漏的。
<6>Handler泄漏
還是先看代碼
內(nèi)部匿名Handler造成的泄漏
我們?nèi)タ匆幌耯andler的源碼找一下原因:
Message的target
每個(gè)Message在放入looper里面時(shí)骤星,都會(huì)為這個(gè)message指定一個(gè)target经瓷,而這個(gè)target就是Handler,如果這個(gè)handler是一個(gè)內(nèi)部類洞难,就會(huì)造成對(duì)應(yīng)的外部類泄漏舆吮。
我們看一下這個(gè)問題在網(wǎng)上的解決辦法:
弱引用法解決
官方提供的方法解決(用的不對(duì),不要參考)
思考兩個(gè)問題:弱引用是解決內(nèi)存泄漏的首先方法嗎队贱?這里使用的官方的方法會(huì)出什么問題色冀?
(2)注冊(cè)和反注冊(cè)
平時(shí)往單例中注冊(cè)一些監(jiān)聽,正常都要在適當(dāng)?shù)臅r(shí)候進(jìn)行反注冊(cè)柱嫌,除非這個(gè)監(jiān)聽是要伴隨著整個(gè)進(jìn)程的生命周期锋恬,這個(gè)比較容易理解,也是靜態(tài)持有導(dǎo)致编丘。
另一種注冊(cè)和反注冊(cè)的情況是receiver,Receiver正常是注冊(cè)到系統(tǒng)中了瘪吏,那到底被誰持有了呢
被loadedApk持有receiver
關(guān)于loadedApk這個(gè)類是在ActivityThread中初始化的癣防,具體它的作用,可以在網(wǎng)上查找一下掌眠。
對(duì)于注冊(cè)到localBroadcastManager中的Receiver蕾盯,就更簡(jiǎn)單了,因?yàn)檫@個(gè)receiver在app內(nèi)部使用蓝丙,所以它就是一個(gè)類似往單例中注冊(cè)listener的形式级遭,必須反注冊(cè)的。
(3)資源對(duì)象沒關(guān)閉造成的內(nèi)存泄露
這種情況的內(nèi)存泄漏渺尘,就是我們開始說的向系統(tǒng)申請(qǐng)資源后沒釋放的情況挫鸽,常見的是流和數(shù)據(jù)庫(kù)未關(guān)閉,對(duì)于這種情況的細(xì)節(jié)就不做代碼分析了鸥跟,我的理解是:
linux對(duì)于每個(gè)設(shè)備等都是以文件來對(duì)待的丢郊,所以不管是文件還是設(shè)備控漠,在打開時(shí)苏携,系統(tǒng)都會(huì)為它創(chuàng)建一定的buffer,這個(gè)buffer是要占用內(nèi)存空間的,如果沒有關(guān)閉對(duì)應(yīng)的流形导,這個(gè)buffer空間是一直被占用的捕透。
(4)Bitmap的recycle不調(diào)會(huì)導(dǎo)致泄漏嗎?
這一項(xiàng)是打了問號(hào)的论颅,即到底Bitmap會(huì)不會(huì)造成內(nèi)存泄漏呢朴下?我們來一點(diǎn)點(diǎn)分析:
對(duì)于Bitmap的recycle()方法需不需要調(diào)用,網(wǎng)上的說法一般是這樣的:
bitmap 2.3以前調(diào)角虫,以后不調(diào)
那沾谓,如果在2.3及以下,不顯式的調(diào)用recycle()戳鹅,是不是就內(nèi)存泄漏了呢搏屑?
我覺得不會(huì),因?yàn)楣俜綄?duì)recycle()的解釋里面粉楚,從沒說必須要調(diào),只是推薦亮垫。
官方2.3及以下圖片內(nèi)存管理的說明
2.3源碼中對(duì)recycle()的說明
從這兩份文檔來看模软,官方的意思應(yīng)該是,這是一個(gè)高級(jí)調(diào)用饮潦,平時(shí)是不需要顯式調(diào)用的燃异,gc回回收這部分內(nèi)存的,但2.3及以下继蜡,如果你確認(rèn)一個(gè)bitmap的確不用了回俐,還是調(diào)一下recycle比較好。
有點(diǎn)把人搞糊涂了稀并,一般大家都認(rèn)為2.3以下bitmap內(nèi)存是native的堆中仅颇,gc收集不到,所以會(huì)引發(fā)一些OOM碘举,但文檔里又說GC會(huì)收集這些內(nèi)存忘瓦,讓我們不用擔(dān)心,到底是怎么一會(huì)事兒呢引颈?
我們還是看源碼吧耕皮,看源碼能解決我們所有的疑惑,每次去看源碼時(shí)蝙场,總能想來來Linus的那句話凌停,好像是"Talk is cheap. Show me the code.",還有"Read the Fucking Source Code"售滤。
Bitmap的源碼
bitmap構(gòu)造函數(shù)中初始化這樣一個(gè)對(duì)象
nativeDestructor方法中釋放內(nèi)存
源碼地址:
到了這里罚拟,大家可以再回過頭思考一個(gè)問題,為什么2.3及以下的bitmap內(nèi)存也不會(huì)泄露,可大家還總是會(huì)說2.3的圖片分配在native舟舒,容易造成OOM呢拉庶?
5、MAT分析內(nèi)存泄漏
對(duì)于內(nèi)存問題的分析秃励,AndroidStudio也提供了dump工具氏仗,但功能與mat比起來還是要弱很多,所以我平時(shí)還是習(xí)慣使用MAT來進(jìn)行分析夺鲜。
打開 DDMS 工具皆尔,在左邊 Devices 視圖頁(yè)面選中“Update Heap”圖標(biāo),然后在右邊切換到 Heap 視圖币励,點(diǎn)擊 Heap 視圖中的“Cause GC”按鈕慷蠕,到此為止需檢測(cè)的進(jìn)程就可以被監(jiān)視。
ddms
Heap視圖中部有一個(gè)Type叫做data object食呻,即數(shù)據(jù)對(duì)象流炕,也就是我們的程序中大量存在的類類型的對(duì)象。在data object一行中有一列是“Total Size”仅胞,其值就是當(dāng)前進(jìn)程中所有Java數(shù)據(jù)對(duì)象的內(nèi)存總量每辟,一般情況下,這個(gè)值的大小決定了是否會(huì)有內(nèi)存泄漏干旧∏郏可以這樣判斷:
進(jìn)入某應(yīng)用,不斷的操作該應(yīng)用椎眯,同時(shí)注意觀察data object的Total Size值挠将,正常情況下Total Size值都會(huì)穩(wěn)定在一個(gè)有限的范圍內(nèi),也就是說由于程序中的的代碼良好编整,沒有造成對(duì)象不被垃圾回收的情況舔稀。
所以說雖然我們不斷的操作會(huì)不斷的生成很多對(duì)象,而在虛擬機(jī)不斷的進(jìn)行GC的過程中掌测,這些對(duì)象都被回收了镶蹋,內(nèi)存占用量會(huì)會(huì)落到一個(gè)穩(wěn)定的水平;反之如果代碼中存在沒有釋放對(duì)象引用的情況赏半,則data object的Total Size值在每次GC后不會(huì)有明顯的回落贺归。隨著操作次數(shù)的增多Total Size的值會(huì)越來越大,直到到達(dá)一個(gè)上限后導(dǎo)致進(jìn)程被殺掉断箫。
MAT分析hprof來定位內(nèi)存泄露的原因所在
這是出現(xiàn)內(nèi)存泄露后使用MAT進(jìn)行問題定位的有效手段拂酣。
A)Dump出內(nèi)存泄露當(dāng)時(shí)的內(nèi)存鏡像hprof,分析懷疑泄露的類:
dump內(nèi)存
注意:這里dump出來的hprof文件仲义,要想直接查看婶熬,是需要在eclipse中安裝mat插件的剑勾;這也帶來一個(gè)問題,要想方便查看赵颅,是要打開eclipse的虽另,但eclipse與androidstudio是不兼容的,打開了一個(gè)饺谬,另一個(gè)的adb就連不上捂刺,這塊的確比較麻煩。
B)使用OQL募寨,查詢內(nèi)存中的對(duì)象:
使用OQL
我們?cè)诓樵儍?nèi)存泄漏時(shí)族展,一般優(yōu)先是看Activity,它持有的內(nèi)存是四大組件中最多的拔鹰,也是我們平時(shí)最容易出現(xiàn)的內(nèi)存泄漏仪缸,為了快速查找出這類的對(duì)象,我們可以使用OQL來寫列肢。
C)分析這些持有引用的對(duì)象的GC路徑
查詢引用關(guān)系
D)逐個(gè)分析每個(gè)對(duì)象的GC路徑是否正常
分析引用持有路徑
從這個(gè)路徑可以看出是一個(gè)antiRadiationUtil工具類對(duì)象持有了MainActivity的引用導(dǎo)致MainActivity無法釋放恰画。此時(shí)就要進(jìn)入代碼分析此時(shí)antiRadiationUtil的引用持有是否合理(如果antiRadiationUtil持有了MainActivity的context導(dǎo)致節(jié)目退出后MainActivity無法銷毀,那一般都屬于內(nèi)存泄露了)瓷马。
E)其它的使用:分析持有此類對(duì)象引用的外部對(duì)象
查詢外部引用拴还。