內(nèi)存泄露相關(guān)

·# 內(nèi)存

對(duì)用戶體驗(yàn)影響

  1. 可能oom
  2. 內(nèi)存過大被LMK機(jī)制殺死。
  3. 內(nèi)存抖動(dòng)造成程序卡頓

原理

內(nèi)存分配策略

首先我們來了解程序運(yùn)行時(shí),所需內(nèi)存的分配策略:

按照編譯原理的觀點(diǎn),程序運(yùn)行時(shí)的內(nèi)存分配有三種策略,分別是靜態(tài)的,棧式的,和堆式的,對(duì)應(yīng)的,三種存儲(chǔ)策略使用的內(nèi)存空間主要分別是靜態(tài)存儲(chǔ)區(qū)(也稱方法區(qū))、堆區(qū)和棧區(qū)。他們的功能不同,對(duì)他們使用方式也就不同。

靜態(tài)存儲(chǔ)區(qū)(方法區(qū)):內(nèi)存在程序編譯的時(shí)候就已經(jīng)分配好,這塊內(nèi)存在程序整個(gè)運(yùn)行期間都存在。它主要存放靜態(tài)數(shù)據(jù)、全局static數(shù)據(jù)和常量。

棧區(qū):在執(zhí)行函數(shù)時(shí),函數(shù)內(nèi)局部變量的存儲(chǔ)單元都可以在棧上創(chuàng)建,函數(shù)執(zhí)行結(jié)束時(shí)這些存儲(chǔ)單元自動(dòng)被釋放。棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。

堆區(qū):亦稱動(dòng)態(tài)內(nèi)存分配。程序在運(yùn)行的時(shí)候用malloc或new申請(qǐng)任意大小的內(nèi)存,程序員自己負(fù)責(zé)在適當(dāng)?shù)臅r(shí)候用free或delete釋放內(nèi)存(Java則依賴?yán)厥掌鳎?dòng)態(tài)內(nèi)存的生存期可以由我們決定,如果我們不釋放內(nèi)存,程序?qū)⒃谧詈蟛裴尫诺魟?dòng)態(tài)內(nèi)存。 但是,良好的編程習(xí)慣是:如果某動(dòng)態(tài)內(nèi)存不再使用,需要將其釋放掉。
在函數(shù)中(說明是局部變量)定義的一些基本類型的變量和對(duì)象的引用變量都是在函數(shù)的棧內(nèi)存中分配。當(dāng)在一段代碼塊中定義一個(gè)變量時(shí),java就在棧中為這個(gè)變量分配內(nèi)存空間,當(dāng)超過變量的作用域后,java會(huì)自動(dòng)釋放掉為該變量分配的內(nèi)存空間,該內(nèi)存空間可以立刻被另作他用。

堆內(nèi)存用于存放所有由new創(chuàng)建的對(duì)象(內(nèi)容包括該對(duì)象其中的所有成員變量)和數(shù)組。在堆中分配的內(nèi)存,由java虛擬機(jī)自動(dòng)垃圾回收器來管理。在堆中產(chǎn)生了一個(gè)數(shù)組或者對(duì)象后,還可以在棧中定義一個(gè)特殊的變量,這個(gè)變量的取值等于數(shù)組或者對(duì)象在堆內(nèi)存中的首地址,在棧中的這個(gè)特殊的變量就變成了數(shù)組或者對(duì)象的引用變量,以后就可以在程序中使用棧內(nèi)存中的引用變量來訪問堆中的數(shù)組或者對(duì)象,引用變量相當(dāng)于為數(shù)組或者對(duì)象起的一個(gè)別名,或者代號(hào)。

堆是不連續(xù)的內(nèi)存區(qū)域(因?yàn)橄到y(tǒng)是用鏈表來存儲(chǔ)空閑內(nèi)存地址,自然不是連續(xù)的),堆大小受限于計(jì)算機(jī)系統(tǒng)中有效的虛擬內(nèi)存(32bit系統(tǒng)理論上是4G),所以堆的空間比較靈活,比較大。棧是一塊連續(xù)的內(nèi)存區(qū)域,大小是操作系統(tǒng)預(yù)定好的,windows下棧大小是2M(也有是1M,在編譯時(shí)確定,VC中可設(shè)置)。

為什么內(nèi)存泄漏

為了判斷Java中是否有內(nèi)存泄露,我們首先必須了解Java是如何管理(堆)內(nèi)存的。Java的內(nèi)存管理就是對(duì)象的分配和釋放問題。在Java中,內(nèi)存的分配是由程序完成的,而內(nèi)存的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程序員不需要通過調(diào)用函數(shù)來釋放內(nèi)存,但它只能回收無用并且不再被其它對(duì)象引用的那些對(duì)象所占用的空間。

Java的內(nèi)存垃圾回收機(jī)制是從程序的主要運(yùn)行對(duì)象(如靜態(tài)對(duì)象/寄存器/棧上指向的堆內(nèi)存對(duì)象等)開始檢查引用鏈,當(dāng)遍歷一遍后得到上述這些無法回收的對(duì)象和他們所引用的對(duì)象鏈,組成無法回收的對(duì)象集合,而其他孤立對(duì)象(集)就作為垃圾回收。GC為了能夠正確釋放對(duì)象,必須監(jiān)控每一個(gè)對(duì)象的運(yùn)行狀態(tài),包括對(duì)象的申請(qǐng)、引用、被引用、賦值等,GC都需要進(jìn)行監(jiān)控。監(jiān)視對(duì)象狀態(tài)是為了更加準(zhǔn)確地、及時(shí)地釋放對(duì)象,而釋放對(duì)象的根本原則就是該對(duì)象不再被引用。

在Java中,這些無用的對(duì)象都由GC負(fù)責(zé)回收,因此程序員不需要考慮這部分的內(nèi)存泄露。雖然,我們有幾個(gè)函數(shù)可以訪問GC,例如運(yùn)行GC的函數(shù)System.gc(),但是根據(jù)Java語言規(guī)范定義,該函數(shù)不保證JVM的垃圾收集器一定會(huì)執(zhí)行。因?yàn)椴煌腏VM實(shí)現(xiàn)者可能使用不同的算法管理GC。通常GC的線程的優(yōu)先級(jí)別較低。JVM調(diào)用GC的策略也有很多種,有的是內(nèi)存使用到達(dá)一定程度時(shí),GC才開始工作,也有定時(shí)執(zhí)行的,有的是平緩執(zhí)行GC,有的是中斷式執(zhí)行GC。但通常來說,我們不需要關(guān)心這些。

堆內(nèi)存中的長生命周期的對(duì)象持有短生命周期對(duì)象的強(qiáng)/軟引用,盡管短生命周期對(duì)象已經(jīng)不再需要,但是因?yàn)殚L生命周期對(duì)象持有它的引用而導(dǎo)致不能被回收,這就是Java中內(nèi)存泄露的根本原因`。

內(nèi)存泄漏常見原因

集合類

集合類如果僅僅有添加元素的方法,而沒有相應(yīng)的刪除機(jī)制,導(dǎo)致內(nèi)存被占用。如果這個(gè)集合類是全局性的變量 (比如類中的靜態(tài)屬性,全局性的 map 等即有靜態(tài)引用或 final 一直指向它),那么沒有相應(yīng)的刪除機(jī)制,很可能導(dǎo)致集合所占用的內(nèi)存只增不減。

Android 組件或特殊集合對(duì)象的使用

BroadcastReceiver,ContentObserver,F(xiàn)ileObserver,Cursor,Callback等在 Activity onDestroy 或者某類生命周期結(jié)束之后一定要 unregister 或者 close 掉,否則這個(gè) Activity 類會(huì)被 system 強(qiáng)引用,不會(huì)被內(nèi)存回收。
不要直接對(duì) Activity 進(jìn)行直接引用作為成員變量,如果不得不這么做,請(qǐng)用 private WeakReference mActivity 來做,相同的,對(duì)于Service 等其他有自己聲明周期的對(duì)象來說,直接引用都需要謹(jǐn)慎考慮是否會(huì)存在內(nèi)存泄露的可能。

Handler

要知道,只要 Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對(duì)象將被線程 MessageQueue 一直持有。由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的。因此這種實(shí)現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導(dǎo)致無法正確釋放。如上所述,Handler 的使用要尤為小心,否則將很容易導(dǎo)致內(nèi)存泄露的發(fā)生。

Thread 內(nèi)存泄露

線程也是造成內(nèi)存泄露的一個(gè)重要的源頭。線程產(chǎn)生內(nèi)存泄露的主要原因在于線程生命周期的不可控。比如線程是 Activity 的內(nèi)部類,則線程對(duì)象中保存了 Activity 的一個(gè)引用,當(dāng)線程的 run 函數(shù)耗時(shí)較長沒有結(jié)束時(shí),線程對(duì)象是不會(huì)被銷毀的,因此它所引用的老的 Activity 也不會(huì)被銷毀,因此就出現(xiàn)了內(nèi)存泄露的問題。

異步線程未完成前退出 Activity 等組件,可能會(huì)導(dǎo)致界面資源無法釋放。

這種情況是典型的線程對(duì)象導(dǎo)致的內(nèi)存泄露。原因也很簡單,線程 Thread 對(duì)象的 run 任務(wù)未執(zhí)行完之前,對(duì)象本身是不會(huì)釋放的。因此 Activity 等組件對(duì)象內(nèi)的線程對(duì)象成員如果有耗時(shí)任務(wù)(一般也都是耗時(shí)任務(wù)),就會(huì)導(dǎo)致一直持有組件本身的引用內(nèi)存泄露!
本文部分內(nèi)容和經(jīng)驗(yàn)摘自網(wǎng)絡(luò),結(jié)合本次內(nèi)存泄露的排查總結(jié)予以歸納

工具

AS的Memory窗口

可以圖表的方式顯示內(nèi)存情況 - 空閑內(nèi)存 & 已使用內(nèi)存.

adb shell dumpsys meminfo com.mogujie

非常好的工具 可以顯示java heap 、 native heap、共享內(nèi)存、堆棧信息。

MAT

Eclipse Memory Analysis Tools(點(diǎn)我下載)是一個(gè)專門分析Java堆數(shù)據(jù)內(nèi)存引用的工具,我們可以使用它方便的定位內(nèi)存泄露原因,核心任務(wù)就是找到GC ROOT位置即可,哎呀,關(guān)于這個(gè)工具的使用我是真的不想說了,自己搜索吧,實(shí)在簡單、傳統(tǒng)的不行了。

PS:這是開發(fā)中使用頻率非常高的一個(gè)工具之一,麻煩務(wù)必掌握其核心使用技巧

DDMS-Heap

DDMS 自帶的內(nèi)存分析工具 比較簡單 使用頻度不高

leakcanary

leakcanary是一個(gè)開源項(xiàng)目,一個(gè)內(nèi)存泄露自動(dòng)檢測(cè)工具,是著名的GitHub開源組織Square貢獻(xiàn)的,它的主要優(yōu)勢(shì)就在于自動(dòng)化過早的發(fā)覺內(nèi)存泄露、配置簡單、抓取貼心,缺點(diǎn)在于還存在一些bug,不過正常使用百分之九十情況是OK的,其核心原理與MAT工具類似。

監(jiān)控

目前沒有對(duì)內(nèi)存進(jìn)行有效的監(jiān)控。

線上oom 統(tǒng)計(jì)

對(duì)線上所有發(fā)生的oom進(jìn)行統(tǒng)計(jì),同時(shí)記錄所有的activity的調(diào)用周期 和 可能的內(nèi)存泄漏場(chǎng)景

線上內(nèi)存統(tǒng)計(jì)

統(tǒng)計(jì)每個(gè)ativity的內(nèi)存使用情況

其他

優(yōu)化點(diǎn)

兜底回收內(nèi)存

Activity泄漏會(huì)導(dǎo)致該Activity引用到的Bitmap、DrawingCache等無法釋放,對(duì)內(nèi)存造成大的壓力,兜底回收是指對(duì)于已泄漏Activity,嘗試回收其持有的資源,泄漏的僅僅是一個(gè)Activity空殼,從而降低對(duì)內(nèi)存的壓力。
做法也非常簡單,在Activity onDestory時(shí)候從view的rootview開始,遞歸釋放所有子view涉及的圖片,背景,DrawingCache,監(jiān)聽器等等資源,讓Activity成為一個(gè)不占資源的空殼,泄露了也不會(huì)導(dǎo)致圖片資源被持有。

fresco

使用fresco,圖片信息保存到共享內(nèi)存上。減少java heap使用

圖片按需加載

即圖片的大小不應(yīng)該超過view的大小。在把圖片載入內(nèi)存之前,我們需要先計(jì)算出一個(gè)合適的inSampleSize縮放比例,避免不必要的大圖載入。對(duì)此,我們可以重載drawable與ImageView,例如在Activity ondestroy時(shí),檢測(cè)圖片大小與View的大小,若超過可以上報(bào)或提示。

oom處理

我們可以在加載bitmap時(shí),若發(fā)生OOM(try catch方式),可以通過清除cache,降低bitmap format(ARGB8888/RBG565/ARGB4444/ALPHA8)等方式,重新嘗試。

低內(nèi)存處理

對(duì)于系統(tǒng)函數(shù)onLowMemory等函數(shù)是針對(duì)整個(gè)系統(tǒng)而已的,對(duì)于本進(jìn)程來說,其dalvik內(nèi)存距離OOM的差值并沒有體現(xiàn),也沒有回調(diào)函數(shù)供我們及時(shí)釋放內(nèi)存。假若能有那么一套機(jī)制,可以實(shí)時(shí)監(jiān)控進(jìn)程的堆內(nèi)存使用率,達(dá)到設(shè)定值即關(guān)于通知相關(guān)模塊進(jìn)行內(nèi)存釋放,這會(huì)大大的降低OOM。
? 實(shí)現(xiàn)原理
這個(gè)其實(shí)比較簡單,通過Runtime獲得maxMemory,而totalMemory-freeMemory即為當(dāng)前真正使用的dalvik內(nèi)存。
Runtime.getRuntime().maxMemory();
Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
? 操作方式
我們可以定期(前臺(tái)每隔3分鐘)去得到這個(gè)值,當(dāng)我們這個(gè)值達(dá)到危險(xiǎn)值時(shí)(例如80%),我們應(yīng)當(dāng)主要去釋放我們的各種cache資源(bitmap的cache為大頭),同時(shí)顯示的去Trim應(yīng)用的memory,加速內(nèi)存收集。
WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);

多進(jìn)程

對(duì)于webview,圖庫等,由于存在內(nèi)存系統(tǒng)泄露或者占用內(nèi)存過多的問題,我們可以采用單獨(dú)的進(jìn)程。微信當(dāng)前也會(huì)把它們放在單獨(dú)的tools進(jìn)程中

代碼實(shí)踐

對(duì) Activity 等組件的引用應(yīng)該控制在 Activity 的生命周期之內(nèi); 如果不能就考慮使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部長生命周期的對(duì)象引用而泄露

警惕線程未終止造成的內(nèi)存泄露;譬如在Activity中關(guān)聯(lián)了一個(gè)生命周期超過Activity的Thread,在退出Activity時(shí)切記結(jié)束線程。一個(gè)典型的例子就是HandlerThread的run方法是一個(gè)死循環(huán),它不會(huì)自己結(jié)束,線程的生命周期超過了Activity生命周期,我們必須手動(dòng)在Activity的銷毀方法中中調(diào)運(yùn)thread.getLooper().quit();才不會(huì)泄露。

對(duì)象的注冊(cè)與反注冊(cè)沒有成對(duì)出現(xiàn)造成的內(nèi)存泄露;譬如注冊(cè)廣播接收器、注冊(cè)觀察者(典型的譬如數(shù)據(jù)庫的監(jiān)聽)等。

創(chuàng)建與關(guān)閉沒有成對(duì)出現(xiàn)造成的泄露;譬如Cursor資源必須手動(dòng)關(guān)閉,WebView必須手動(dòng)銷毀,流等對(duì)象必須手動(dòng)關(guān)閉等。

在代碼復(fù)審的時(shí)候關(guān)注長生命周期對(duì)象:全局性的集合、單例模式的使用、類的 static 變量等等。

線程 Runnable 執(zhí)行耗時(shí)操作,注意在頁面返回時(shí)及時(shí)取消或者把 Runnable 寫成靜態(tài)類。
a) 如果線程類是內(nèi)部類,改為靜態(tài)內(nèi)部類。
b) 線程內(nèi)如果需要引用外部類對(duì)象如 context,需要使用弱引用

Handler 的持有的引用對(duì)象最好使用弱引用,資源釋放時(shí)也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的時(shí)候,取消掉該 Handler 對(duì)象的 Message和 Runnable.

總結(jié)

我們并不能將內(nèi)存優(yōu)化中用到的所有技巧都一一說明,而且隨著Android版本的更替,可能很多方法都會(huì)變的過時(shí)。我在想更重要的是我們能持續(xù)的發(fā)現(xiàn)問題,精細(xì)化的監(jiān)控,而不是一直處于"哪個(gè)有坑填哪里的"的窘?jīng)r。在這里給大家的建議有:
1 率先考慮采用已有的工具;中國人喜歡重復(fù)造輪子,我們更推薦花精力去優(yōu)化已有工具,為廣大碼農(nóng)做貢獻(xiàn)。生活已不易,碼農(nóng)何為為難碼農(nóng)!
2 不拘泥于點(diǎn),更重要在于如何建立合理的框架避免發(fā)生問題,或者是能及時(shí)的發(fā)現(xiàn)問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評(píng)論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,634評(píng)論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評(píng)論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,835評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,235評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,459評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,000評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,819評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,004評(píng)論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,257評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評(píng)論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,717評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,003評(píng)論 2 374

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