內存泄漏全解析


今日科技快訊

上海市網信辦會同市文廣局、市網安總隊、市文化執法總隊,就直播中的低俗色情問題依法聯合約談熊貓直播和全民直播,要求上述直播網站進行全面整改。熊貓直播方面表示,此次整改主要是一些彈幕的問題,而非直播內容問題,后續會進行一套配合工作,總體會圍繞彈幕氛圍的引導來做。

作者簡介

本篇是 南塵 的第二篇投稿,全方位分析了導致內存泄漏的原因,並給出解決方案。希望能夠幫助到大家。

南塵 的博客地址:

http://www.cnblogs.com/liushilin

寫在前面

對於 C++ 來說,內存泄漏就是new出來的對象沒有 delete,俗稱野指針;而對於 java 來說,就是 new 出來的 Object 放在 Heap 上無法被GC回收;而這里就把我之前的一篇內存泄漏的總結翻新,做一個更加全面規范的講解,希望能幫到各位。

 

一些雜談

java的內存分配

  • 靜態存儲區:編譯時就分配好,在程序整個運行期間都存在。它主要存放靜態數據和常量;

  • 棧區:當方法執行時,會在棧區內存中創建方法體內部的局部變量,方法結束后自動釋放內存;

  • 堆區:通常用來存放new出來的對象。由java垃圾回收期回收。

四種引用類型的介紹

  • 強引用(StrongReference):JVM 寧可拋出 OOM ,也不會讓 GC 回收具有強引用的對象;

  • 軟引用(SoftReference):只有在內存空間不足時,才會被回的對象;

  • 弱引用(WeakReference):在 GC 時,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存;

  • 虛引用(PhantomReference):任何時候都可以被GC回收,當垃圾回收器准備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否存在該對象的虛引用,來了解這個對象是否將要被回收。可以用來作為GC回收Object的標志。

內存抖動

這樣的圖很熟悉有木有?當這樣的時候,說明你的內存真的在變少了。

 

內存泄漏檢測工具

1. MAT(Memory Analyzer Tool),下載地址:

http://www.eclipse.org/mat/downloads.php

具體使用這個網上一大篇,樓主一有時間也會為大家一步步奉上。

2. 強大的開源內存檢測工具 LeakCanary。

leakcanary 是一個開源項目,一個內存泄露自動檢測工具,是著名的 GitHub 開源組織 Square 貢獻的,它的主要優勢就在於自動化過早的發覺內存泄露、配置簡單、抓取貼心,缺點在於還存在一些bug,不過正常使用百分之九十情況是OK的,其核心原理與MAT工具類似。

因為配置十分簡單,只需要幾句話就好!!!這里就不多說了,大家可以看官方文檔:

https://github.com/square/leakcanary

 

內存泄漏解析

永遠的Singleton

單例的使用在我們的程序中隨處可見,因為使用它可以完美的解決我們在程序中重復創建對象的問題,不過可別小瞧它。由於單例的靜態特性,使得它的生命周期和應用的生命周期會一樣長,所以一旦使用有誤,小心無限制的持有Activity的引用而導致內存泄漏。比如,下面的例子。

這個錯誤在生活中再普遍不過,很正常的一個單例模式,可就由於傳入的是一個 Context,而這個 Context 的生命周期的長短就尤為重要了。如果我們傳入的是 Activity 的 Context,當這個 Context 所對應的 Activity 退出的時候,由於該 Context 的引用被單例對象所持有,其生命周期等於整個應用程序的生命周期,所以當前 Activity 退出時它的內存並不會回收,這造成的內存泄漏就可想而知了。

正確的方式應該是把傳入的 Context 換為和應用的生命周期一樣長的 Application 的 Context:

當然,你也可以直接連 Context 都不用傳入了。重寫 application,提供靜態的 getContext 方法:

自然就可以直接不用傳入Context:

令人心塞的Handler

這個東西在我最近遇到的最多了,而它也是我們在內存泄漏中最為常見的,也許你的一個小忽略就會導致內存泄漏。在Android的新版本中,我們被要求必須把網絡任務等耗時操作置於新線程來處理,我們通常會采用 Handler。

但 Handler 不是萬能的,若是我們的編寫不規范就有可能會造成內存泄漏。另外,我們知道,Handler、Message 和 MessageQueue 都是相互關聯在一起的,萬一 Handler 發送的 Message 尚未被處理,則該 Message 及發送它的 Handler 對象將會被線程 MessageQueue 一直持有。

由於 Handler 屬於 TLS(Thread Local Storage)變量,生命周期和 Activity 是不一致的。因此這種實現方式一般很難保證跟 View 或者 Activity 的生命周期一致,故很容易導致無法正確釋放。比如:

在例子中,我們申明了一個 延遲5分鍾 執行的消息 Message。當該 Activity 被 finish 的時候,延遲任務的 Message 還存在於主線程中,它持有該 Activity 的 Handler 引用,所以此時 finish 掉的 Activity 就不會回收了,所以造成了內存泄漏(因 handler 為非靜態內部類,它會持有外部類的引用,在這里就是當前的 Activity)。

修復:這個解決也是可以通過把其聲明為 static 的,則其存活期就跟 activity 的生命周期無關了。不過倘若用到 Context 等外部類的 非static 對象,還是應該通過弱引用傳入。比如:

綜述:推薦使用靜態內部類+弱引用 WeakReference 這種方式,但要注意每次使用前判空。

說到若引用,這里再提下java的幾種引用類型:Strong, reference,SoftReference,WeakReference 和 PhatomReference:

在 Android 開發中,為了防止內存溢出,在處理一些占用內存大並且生命周期較長的對象的時候,可以盡量地使用 軟引用 和 弱引用 技術。

比如,保存 Bitmap 的軟引用到 HashMap:

使用軟引用以后,在 OutOfMemory 異常發生之前,這些緩存的圖片資源的內存空間可以被釋放掉的,從而避免內存達到上限,避免Crash發生。

如果只是想避免 OutOfMemory 異常的發生,則可以使用 軟引用。如果對於應用的性能更在意,想盡快回收一些占用內存比較大的對象,則可以使用 弱引用。

另外可以根據對象是否經常使用來判斷選擇 軟引用 還是 弱引用。如果該對象可能會經常使用的,就盡量用 軟引用。如果該對象不被使用的可能性更大些,就可以用 弱引用。

ok,繼續回到主題。前面所說的,創建一個 靜態Handler內部類,然后對 Handler 持有的對象使用弱引用,這樣在回收時也可以回收 Handler 持有的對象,但是這樣做雖然避免了 Activity 泄漏,不過 Looper 線程的消息隊列中還是可能會有待處理的消息,所以我們在 Activity 的 Destroy 時或者 Stop 時應該移除消息隊列 MessageQueue 中的消息。

下面幾個方法都可以移除 Message:

匿名內部類/非靜態內部類

它們方便卻暗藏殺機Android開發經常會繼承實現 Activity 或者 Fragment 或者 View。如果你使用了匿名類,而又被異步線程所引用,那得小心,如果沒有任何措施同樣會導致內存泄漏的:

runnable1 和 runnable2 的區別就是,runnable2 使用了匿名內部類,我們看看引用時的引用內存:

可以看到,runnable1 是沒有什么特別的。但 runnable2 多出了一個 MainActivity 的引用,若是這個引用再傳入到一個異步線程,此線程在和Activity生命周期不一致的時候,也就造成了Activity的泄露。

前輩箴言——善用static成員變量

前面就很明顯,當我們的成員變量是 static 的時候,那么它的生命周期將和整個app的生命周期一致。

這必然會導致一系列問題,如果你的app進程設計上是長駐內存的,那即使app切到后台,這部分內存也不會被釋放。按照現在手機app內存管理機制,占內存較大的后台進程將優先回收,因為如果此app做過進程互保保活,那會造成app在后台頻繁重啟。當手機安裝了你參與開發的app以后一夜時間手機被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。

這里修復的方法是:

不要在類初始時初始化靜態成員。可以考慮 lazy初始化(延遲加載)。架構設計上要思考是否真的有必要這樣做,盡量避免。如果架構需要這么設計,那么此對象的生命周期你有責任管理起來。

遠離非靜態內部類和匿名類,多用private static class

在我們的日常代碼中,這樣的情況似乎很常見,及直接寫一個class就這么光禿禿的情況。

這樣就在 Activity 內部創建了一個非靜態內部類的單例,每次啟動 Activity 時都會使用該單例的數據,這樣雖然避免了資源的重復創建,不過這種寫法卻會造成內存泄漏,因為非靜態內部類默認會持有外部類的引用,而該非靜態內部類又創建了一個靜態的實例,該實例的生命周期和應用的一樣長,這就導致了該靜態實例一直會持有該 Activity 的引用,導致 Activity 的內存資源不能正常回收。正確的做法為:

將該內部類設為靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用 Context,請按照上面推薦的使用 Application 的 Context。當然,Application 的 context  不是萬能的,所以也不能隨便亂用,對於有些地方則必須使用 Activity 的 Context,對於Application,Service,Activity三者的 Context 的應用場景如下:

其中: NO1 表示 Application 和 Service 可以啟動一個 Activity,不過需要創建一個新的 task 任務隊列。而對於 Dialog 而言,只有在 Activity 中才能創建。

集合對象善清除,以免內存泄漏觸不及防

我們通常會把一些對象的引用加入到集合容器(比如 ArrayList)中,當我們不再需要該對象時,並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是 static 的話,那情況就更嚴重了。

所以在退出程序之前,將集合里面的東西 clear,然后置為 null,再退出程序,如下:

webView雖火,內存泄漏卻也火的其所

當我們不再需要使用 webView 的時候,應該調用它的 destory() 方法來銷毀它,並釋放其占用的內存,否則其占用的內存長期也不能回收,從而造成內存泄漏。

解決方案:為 webView 開啟另外一個進程,通過AIDL與主線程進行通信,webView 所在的進程可以根據業務的需要選擇合適的時機進行銷毀,從而達到內存的完整釋放。

而另外一些諸如 listView 的 Adapter 沒有緩存之類的這里就不再多提了。

做一個小的總結

  • 構造 Adapter 時,沒有使用緩存的 convertView

  • Bitmap 對象不在使用時調用 recycle() 釋放內存

  • Context 使用不當造成內存泄露:不要對一個 Activity Context 保持長生命周期的引用。盡量在一切可以使用應用 ApplicationContext 代替 Context 的地方進行替換。

  • 非靜態內部類的靜態實例容易造成內存泄漏:即一個類中如果你不能夠控制它其中內部類的生命周期(譬如Activity中的一些特殊Handler等),則盡量使用靜態類和弱引用來處理(譬如ViewRoot的實現)。

  • 警惕線程未終止造成的內存泄露;譬如在 Activity 中關聯了一個生命周期超過 Activity 的 Thread,在退出 Activity 時切記結束線程。一個典型的例子就是 HandlerThread 的 run 方法是一個死循環,它不會自己結束,線程的生命周期超過了 Activity 生命周期,我們必須手動在Activity的銷毀方法中中調運 thread.getLooper().quit(); 才不會泄露。

  • 對象的注冊與反注冊沒有成對出現造成的內存泄露;譬如注冊廣播接收器、注冊觀察者(典型的譬如數據庫的監聽)等。

  • 創建與關閉沒有成對出現造成的泄露;譬如 Cursor 資源必須手動關閉,WebView 必須手動銷毀,流等對象必須手動關閉等。

  • 不要在執行頻率很高的方法或者循環中創建對象(比如onmeasure),可以使用 HashTable 等創建一組對象容器從容器中取那些對象,而不用每次new與釋放。

  • 避免代碼設計模式的錯誤造成內存泄露;譬如循環引用,A持有B,B持有C,C持有A,這樣的設計誰都得不到釋放。

 

真相只有一個

內存泄漏實在太普遍了,真是防不勝防。不過我想告訴廣大的小伙伴,程序員嘛,誰還不踩個坑,跳出來,拍拍身上的灰塵,總結一下,過兩天又是一條幫幫的coder。你可以的。

更多

每天學習累了,看些搞笑的段子放松一下吧。關注最具娛樂精神的公眾號,每天都有好心情。

如果你有好的技術文章想和大家分享,歡迎向我的公眾號投稿,投稿具體細節請在公眾號主頁點擊“投稿”菜單查看。

歡迎長按下圖 -> 識別圖中二維碼或者掃一掃關注我的公眾號:

閱讀原文
閱讀  3687
20投訴
精選留言

寫留言

  •  5
    我去,本來今天想搶一下沙發呢,我這剛推送過來下面就七八個評論的了!所以我我總結:能否搶到沙發,運氣很重要,真真切切如找工作!但前提條件是你要時時刻刻去關注他們,想着他們,才有可能找到好工作,搶到第一!

    1小時前

     12
    作者回復
    其實吧,搶第一是有技術手段的,我以后可能會寫篇文章來教大家怎么搶

    1小時前

  •  6
    早,郭神

    1小時前

     4
    作者回復
    早,今日第一

    1小時前

  •  4
    怪就怪在我同事他在等地鐵,所以他第一,而我在地鐵上,信號不那么好,要不我第一啊,郭神

    1小時前

     4
    作者回復
    鄧焦是你同事么,他天天都來搶第一

    1小時前

  •  4
    郭神,早!可以推薦兩本深入學習的書嗎?

    1小時前

     4
    作者回復
    就看群英傳和藝術探索這兩本吧

    1小時前

  •  2
    搶什么第一,做好筆記

    50分鍾前

  •  2
    作者給出的單例的demo是非線程安全的。另外我還有一個疑問,作者說的內存分配,四種引用類型都是JVM中的概念,那么在Android的DVM、ART虛擬機中這些機制是否還是一致?如果一致,可以從哪里得到驗證?

    52分鍾前

  •  2
    雖然不是第一,但也是前排啊

    1小時前

  •  2
    是的,每天我倆都是坐在一起的,每天八點他都在惠新西街南口站等換乘,而此時 我已經快到海淀黃庄的公司了,哈哈,他每天都發三個字,而我每天都是在地鐵上收到一直看到公司!不想跟他搶第一

    1小時前

  •  2
    早上好,雖然學習計算機, 但是仍需努力進取,

    1小時前

  •  2
    郭神早

    1小時前

  •  1
    弱引用在安卓中早就不支持了吧!這個文章難道是三年前的?

    51分鍾前

     1
    作者回復
    弱引用不支持了?還真不知道

    49分鍾前

  •  1

    50分鍾前

  •  1
    早早早。。。

    55分鍾前

  •  1

    57分鍾前

  •  1
    簽到

    58分鍾前

  •  1
    總結得很好

    60分鍾前

  •  1
    前排都是沒看文章直奔評論區的…

    1小時前

  •  1
    地鐵上,剛看完鴻神的,再看郭神的

    1小時前

  •  1
    內存泄露,還真是一個沒有注意的重要問題!

    1小時前

  •  1
    郭神早

    1小時前

  •  1
    郭神早~

    1小時前

  •  1
    竟然不是第一

    1小時前

  •  1
    郭神早

    1小時前

  •  1
    webView一直是隱藏的泄漏大戶

    1小時前

  •  1
    早,郭嬸

    1小時前

  •  1
    郭哥早~

    1小時前

  •  1
    第一第一

    1小時前

  •  1
    前排~

    1小時前

  •  1
    今天我前排就坐

    1小時前

  •  
    第二天工作,起太早在車上睡着了!累

    32分鍾前

  •  
    郭神早!

    33分鍾前

  •  
    早,請叫我第一

    35分鍾前

  •  
    bitmap早就不用調用回收了吧?存放的位置已經被Google改變了吧?

    36分鍾前

  •  
    好好看完了

    41分鍾前

  •  
    郭神早

    43分鍾前

  •  
    記得看過谷歌官方的一篇文章,推薦使用lru算法做緩存,忘了是從那個api版本開始,弱引用幾乎起不到什么作用的!

    44分鍾前

  •  
    以前用handler直接注解@SuppressLink(Handlerleak)解決報警問題,現在知道為啥了!

    45分鍾前

  •  
    郭神好。

    1小時前

以上留言由公眾號篩選后顯示

了解留言功能詳情


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM