Android內(nèi)存泄露總結(jié)(附內(nèi)存檢測(cè)工具)
Java 中的內(nèi)存分配
主要是分三塊:
- 靜態(tài)儲(chǔ)存區(qū):編譯時(shí)就分配好,在程序整個(gè)運(yùn)行期間都存在。它主要存放靜態(tài)數(shù)據(jù)和常量。
- 棧區(qū):當(dāng)方法執(zhí)行時(shí),會(huì)在棧區(qū)內(nèi)存中創(chuàng)建方法體內(nèi)部的局部變量,方法結(jié)束后自動(dòng)釋放內(nèi)存。
- 堆區(qū):通常存放 new 出來(lái)的對(duì)象。由 Java 垃圾回收器回收。
棧與堆的區(qū)別
棧內(nèi)存用來(lái)存放局部變量和函數(shù)參數(shù)等。它是先進(jìn)后出的隊(duì)列,進(jìn)出一一對(duì)應(yīng),不產(chǎn)生碎片,運(yùn)行效率穩(wěn)定高。當(dāng)超過(guò)變量的作用域后,該變量也就無(wú)效了,分配給它的內(nèi)存空間也將被釋放掉,該內(nèi)存空間可以被重新使用。
堆內(nèi)存用于存放對(duì)象實(shí)例。在堆中分配的內(nèi)存,將由Java垃圾回收器來(lái)自動(dòng)管理。在堆內(nèi)存中頻繁的 new/delete 會(huì)造成大量?jī)?nèi)存碎片,使程序效率降低。
對(duì)于非靜態(tài)變量的儲(chǔ)存位置,我們可以粗暴的認(rèn)為:
- 局部變量位于棧中(其中引用變量指向的對(duì)象實(shí)體存在于堆中)。
- 成員變量位于堆中。因?yàn)樗鼈儗儆陬悾擃愖罱K被new成對(duì)象,并作為一個(gè)整體儲(chǔ)存在堆中。
四種引用類型的介紹
GC 釋放對(duì)象的根本原則是該對(duì)象不再被引用(強(qiáng)引用)。那么什么是強(qiáng)引用呢?
強(qiáng)引用(Strong Reference)
我們平常用的最多的就是強(qiáng)引用,如下:
- IPhotos iPhotos = new IPhotos();
JVM 寧可拋出 OOM ,也不會(huì)讓 GC 回收具有強(qiáng)引用的對(duì)象。強(qiáng)引用不使用時(shí),可以通過(guò) obj = null 來(lái)顯式的設(shè)置該對(duì)象的所有引用為 null,這樣就可以回收該對(duì)象了。至于什么時(shí)候回收,取決于 GC 的算法,這里不做深究。
軟引用(Soft Reference)
- SoftReference<String> softReference = new SoftReference<>(str);
如果一個(gè)對(duì)象只具有軟引用,那么在內(nèi)存空間足夠時(shí),垃圾回收器就不會(huì)回收它;如果內(nèi)存空間不足了,就會(huì)回收這些對(duì)象的內(nèi)存。只要垃圾回收器沒(méi)有回收它,該對(duì)象就可以被使用。
軟引用曾經(jīng)常被用來(lái)作圖片緩存,然而谷歌現(xiàn)在推薦用 LruCache 替代,因?yàn)?LRU 更高效。
In the past, a popular memory cache implementation was a SoftReference
or WeakReference bitmap cache, however this is not recommended.
Starting from Android 2.3 (API Level 9) the garbage collector is more
aggressive with collecting soft/weak references which makes them
fairly ineffective. In addition, prior to Android 3.0 (API Level 11),
the backing data of a bitmap was stored in native memory which is not
released in a predictable manner, potentially causing an application
to briefly exceed its memory limits and crash. 原文
大致意思是:因?yàn)樵?Android 2.3 以后,GC 會(huì)很頻繁,導(dǎo)致釋放軟引用的頻率也很高,這樣會(huì)降低它的使用效率。并且 3.0 以前 Bitmap 是存放在 Native Memory 中,它的釋放是不受 GC 控制的,所以使用軟引用緩存 Bitmap 可能會(huì)造成 OOM。
弱引用(Weak Reference)
- WeakReference<String> weakReference = new WeakReference<>(str);
與軟引用的區(qū)別在于:只具有弱引用的對(duì)象擁有更短暫的生命周期。因?yàn)樵?GC 時(shí),一旦發(fā)現(xiàn)了只具有弱引用的對(duì)象,不管當(dāng)前內(nèi)存空間足夠與否,都會(huì)回收它的內(nèi)存。不過(guò),由于垃圾回收器是一個(gè)優(yōu)先級(jí)很低的線程,因此不一定會(huì)很快發(fā)現(xiàn)那些只具有弱引用的對(duì)象- -。
虛引用(PhantomReference)
顧名思義,就是形同虛設(shè),與其他幾種引用都不同,虛引用并不會(huì)決定對(duì)象的生命周期,也無(wú)法通過(guò)虛引用獲得對(duì)象實(shí)例。虛引用必須和引用隊(duì)列(ReferenceQueue)聯(lián)合使用。當(dāng)垃圾回收器準(zhǔn)備回收一個(gè)對(duì)象時(shí),如果發(fā)現(xiàn)它還有虛引用,就會(huì)在回收對(duì)象的內(nèi)存之前,把這個(gè)虛引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。程序可以通過(guò)判斷引用隊(duì)列中是否存在該對(duì)象的虛引用,來(lái)了解這個(gè)對(duì)象是否將要被回收。
Android的垃圾回收機(jī)制簡(jiǎn)介
Android 系統(tǒng)里面有一個(gè) Generational Heap Memory 模型,系統(tǒng)會(huì)根據(jù)內(nèi)存中不同的內(nèi)存數(shù)據(jù)類型分別執(zhí)行不同的 GC 操作。
該模型分為三個(gè)區(qū):
- Young Generation
1.eden
2.Survivor Space
1.S0
2.S1 - Old Generation
- Permanent Generation
- Young Generation
大多數(shù) new 出來(lái)的對(duì)象都放到 eden 區(qū),當(dāng) eden 區(qū)填滿時(shí),執(zhí)行 Minor GC(輕量級(jí)GC),然后存活下來(lái)的對(duì)象被轉(zhuǎn)移到 Survivor 區(qū)(有 S0,S1 兩個(gè))。 Minor GC 也會(huì)檢查 Survivor 區(qū)的對(duì)象,并把它們轉(zhuǎn)移到另一個(gè) Survivor 區(qū),這樣就總會(huì)有一個(gè) Survivor 區(qū)是空的。
Old Generation
存放長(zhǎng)期存活下來(lái)的對(duì)象(經(jīng)過(guò)多次 Minor GC 后仍然存活下來(lái)的對(duì)象) Old Generation 區(qū)滿了以后,執(zhí)行 Major GC(大型 GC)。
在Android 2.2 之前,執(zhí)行 GC 時(shí),應(yīng)用的線程會(huì)被暫停,2.3 開(kāi)始添加了并發(fā)垃圾回收機(jī)制。
Permanent Generation
存放方法區(qū)。一般存放:
- 要加載的類的信息
- 靜態(tài)變量
- final常量
- 屬性、方法信息
60 FPS
這里簡(jiǎn)單的介紹一下幀率的概念,以便于理解為什么大量的 GC 容易引起卡頓。
App 開(kāi)發(fā)時(shí),一般追求界面的幀率達(dá)到60 FPS(60 幀/秒),那這個(gè) FPS 是什么概念呢?
- 10-12 FPS 時(shí)可以感受到動(dòng)畫(huà)的效果;
- 24 FPS,可以感受到平滑連貫的動(dòng)畫(huà)效果,電影常用幀率(不追求 60 FPS 是節(jié)省成本);
- 60 FPS,達(dá)到最流暢的效果,對(duì)于更高的FPS,大腦已經(jīng)難以察覺(jué)區(qū)別。
Android 每隔 16 ms發(fā)出 VSYNC 信號(hào),觸發(fā)對(duì) UI 的渲染(即每 16 ms繪制一幀),如果整個(gè)過(guò)程保持在 16 ms以內(nèi),那么就會(huì)達(dá)到 60 FPS 的流暢畫(huà)面。超過(guò)了 16 ms就會(huì)造成卡頓。那么如果在 UI 渲染時(shí)發(fā)生了大量 GC,或者 GC 耗時(shí)太長(zhǎng),那么就可能導(dǎo)致繪制過(guò)程超過(guò) 16 ms從而造成卡頓(FPS 下降、掉幀等),而我們大腦對(duì)于掉幀的情況十分敏銳,因此如果沒(méi)有做好內(nèi)存管理,將會(huì)給用戶帶來(lái)非常不好的體驗(yàn)。
再介紹一下內(nèi)存抖動(dòng)的概念,本文后面可能會(huì)用到這個(gè)概念。
內(nèi)存抖動(dòng)
短時(shí)間內(nèi)大量 new 對(duì)象,達(dá)到 Young Generation 的閾值后觸發(fā)GC,導(dǎo)致剛 new 出來(lái)的對(duì)象又被回收。此現(xiàn)象會(huì)影響幀率,造成卡頓。
內(nèi)存抖動(dòng)在 Android 提供的 Memory Monitor 中大概表現(xiàn)為這樣:
Android中常見(jiàn)的內(nèi)存泄露及解決方案
集合類泄露
如果某個(gè)集合是全局性的變量(比如 static 修飾),集合內(nèi)直接存放一些占用大量?jī)?nèi)存的對(duì)象(而不是通過(guò)弱引用存放),那么隨著集合 size 的增大,會(huì)導(dǎo)致內(nèi)存占用不斷上升,而在 Activity 等銷毀時(shí),集合中的這些對(duì)象無(wú)法被回收,導(dǎo)致內(nèi)存泄露。比如我們喜歡通過(guò)靜態(tài) HashMap 做一些緩存之類的事,這種情況要小心,集合內(nèi)對(duì)象建議采用弱引用的方式存取,并考慮在不需要的時(shí)候手動(dòng)釋放。
單例造成的內(nèi)存泄露
單例的靜態(tài)特性導(dǎo)致其生命周期同應(yīng)用一樣長(zhǎng)。
有時(shí)創(chuàng)建單例時(shí)如果我們需要Context對(duì)象,如果傳入的是Application的Context那么不會(huì)有問(wèn)題。如果傳入的是Activity的Context對(duì)象,那么當(dāng)Activity生命周期結(jié)束時(shí),該Activity的引用依然被單例持有,所以不會(huì)被回收,而單例的生命周期又是跟應(yīng)用一樣長(zhǎng),所以這就造成了內(nèi)存泄露。
解決辦法一:在創(chuàng)建單例的構(gòu)造中不直接用傳進(jìn)來(lái)的context,而是通過(guò)這個(gè)context獲取Application的Context。代碼如下:
- public class AppManager {
- private static AppManager instance;
- private Context context;
- private AppManager(Context context) {
- this.context = context.getApplicationContext();// 使用Application 的context
- }
- public static AppManager getInstance(Context context) {
- if (instance != null) {
- instance = new AppManager(context);
- }
- return instance;
- }
- }
第二種解決方案:在構(gòu)造單例時(shí)不需要傳入 context,直接在我們的 Application 中寫一個(gè)靜態(tài)方法,方法內(nèi)通過(guò) getApplicationContext 返回 context,然后在單例中直接調(diào)用這個(gè)靜態(tài)方法獲取 context。
非靜態(tài)內(nèi)部類造成的內(nèi)存泄露
在 Java 中,非靜態(tài)內(nèi)部類(包括匿名內(nèi)部類,比如 Handler, Runnable匿名內(nèi)部類最容易導(dǎo)致內(nèi)存泄露)會(huì)持有外部類對(duì)象的強(qiáng)引用(如 Activity),而靜態(tài)的內(nèi)部類則不會(huì)引用外部類對(duì)象。
非靜態(tài)內(nèi)部類或匿名類因?yàn)槌钟型獠款惖囊?,所以可以訪問(wèn)外部類的資源屬性成員變量等;靜態(tài)內(nèi)部類不行。
因?yàn)槠胀▋?nèi)部類或匿名類依賴外部類,所以必須先創(chuàng)建外部類,再創(chuàng)建普通內(nèi)部類或匿名類;而靜態(tài)內(nèi)部類隨時(shí)都可以在其他外部類中創(chuàng)建。
Handler內(nèi)存泄露可以關(guān)注我的另一篇專門針對(duì)Handler內(nèi)存泄露的文章:鏈接
WebView 的泄漏
Android 中的 WebView 存在很大的兼容性問(wèn)題,有些 WebView 甚至存在內(nèi)存泄露的問(wèn)題。所以通常***這個(gè)問(wèn)題的辦法是為 WebView 開(kāi)啟另外一個(gè)進(jìn)程,通過(guò) AIDL 與主進(jìn)程進(jìn)行通信, WebView 所在的進(jìn)程可以根據(jù)業(yè)務(wù)的需要選擇合適的時(shí)機(jī)進(jìn)行銷毀,從而達(dá)到內(nèi)存的完整釋放。
AlertDialog 造成的內(nèi)存泄露
- new AlertDialog.Builder(this)
- .setPositiveButton("Baguette", new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- MainActivity.this.makeBread();
- }
- }).show();
DialogInterface.OnClickListener 的匿名實(shí)現(xiàn)類持有了 MainActivity 的引用;
而在 AlertDialog 的實(shí)現(xiàn)中,OnClickListener 類將被包裝在一個(gè) Message 對(duì)象中(具體可以看 AlertController 類的 setButton 方法),而且這個(gè) Message 會(huì)在其內(nèi)部被復(fù)制一份(AlertController 類的 mButtonHandler 中可以看到),兩份 Message 中只有一個(gè)被 recycle,另一個(gè)(OnClickListener 的成員變量引用的 Message 對(duì)象)將會(huì)泄露!
解決辦法:
- Android 5.0 以上不存在此問(wèn)題;
- Message 對(duì)象的泄漏無(wú)法避免,但是如果僅僅是一個(gè)空的 Message 對(duì)象,將被放入對(duì)象池作為后用,是沒(méi)有問(wèn)題的;
- 讓 DialogInterface.OnClickListener 對(duì)象不持有外部類的強(qiáng)引用,如用 static 類實(shí)現(xiàn);
- 在 Activity 退出前 dismiss dialog
Drawable 引起的內(nèi)存泄露
Android 在 4.0 以后已經(jīng)解決了這個(gè)問(wèn)題。這里可以跳過(guò)。
當(dāng)我們屏幕旋轉(zhuǎn)時(shí),默認(rèn)會(huì)銷毀掉當(dāng)前的 Activity,然后創(chuàng)建一個(gè)新的 Activity 并保持之前的狀態(tài)。在這個(gè)過(guò)程中,Android 系統(tǒng)會(huì)重新加載程序的UI視圖和資源。假設(shè)我們有一個(gè)程序用到了一個(gè)很大的 Bitmap 圖像,我們不想每次屏幕旋轉(zhuǎn)時(shí)都重新加載這個(gè) Bitmap 對(duì)象,最簡(jiǎn)單的辦法就是將這個(gè) Bitmap 對(duì)象使用 static 修飾。
- private static Drawable sBackground;
- @Override
- protected void onCreate(Bundle state) {
- super.onCreate(state);
- TextView label = new TextView(this);
- label.setText("Leaks are bad");
- if (sBackground == null) {
- sBackground = getDrawable(R.drawable.large_bitmap);
- }
- label.setBackgroundDrawable(sBackground);
- setContentView(label);
- }
但是上面的方法在屏幕旋轉(zhuǎn)時(shí)有可能引起內(nèi)存泄露,因?yàn)?,?dāng)一個(gè) Drawable 綁定到了 View 上,實(shí)際上這個(gè) View 對(duì)象就會(huì)成為這個(gè) Drawable 的一個(gè) callback 成員變量,上面的例子中靜態(tài)的 sBackground 持有 TextView 對(duì)象的引用,而 TextView 持有 Activity 的引用。當(dāng)屏幕旋轉(zhuǎn)時(shí),Activity 無(wú)法被銷毀,這樣就產(chǎn)生了內(nèi)存泄露問(wèn)題。
該問(wèn)題主要產(chǎn)生在 4.0 以前,因?yàn)樵?2.3.7 及以下版本 Drawable 的 setCallback 方法的實(shí)現(xiàn)是直接賦值,而從 4.0.1 開(kāi)始,setCallback 采用了弱引用處理這個(gè)問(wèn)題,避免了內(nèi)存泄露問(wèn)題。
資源未關(guān)閉造成的內(nèi)存泄露
- BroadcastReceiver,ContentObserver 之類的沒(méi)有解除注冊(cè)
- Cursor,Stream 之類的沒(méi)有 close
- ***循環(huán)的動(dòng)畫(huà)在 Activity 退出前沒(méi)有停止
- 一些其他的該 release 的沒(méi)有 release,該 recycle 的沒(méi)有 recycle… 等等。
總結(jié)
我們不難發(fā)現(xiàn),大多數(shù)問(wèn)題都是 static 造成的!
- 在使用 static 時(shí)一定要小心,關(guān)注該 static 變量持有的引用情況。在必要情況下使用弱引用的方式來(lái)持有一些引用
- 在使用非靜態(tài)內(nèi)部類時(shí)也要注意,畢竟它們持有外部類的引用。(使用 RxJava 的同學(xué)在 subscribe 時(shí)也要注意 unSubscribe)
- 注意在生命周期結(jié)束時(shí)釋放資源
- 使用屬性動(dòng)畫(huà)時(shí),不用的時(shí)候請(qǐng)停止(尤其是循環(huán)播放的動(dòng)畫(huà)),不然會(huì)產(chǎn)生內(nèi)存泄露(Activity 無(wú)法釋放)(View 動(dòng)畫(huà)不會(huì))
幾種內(nèi)存檢測(cè)工具的介紹
- Memory Monitor
- Allocation Tracker
- Heap Viewer
- LeakCanary
Memory Monitor
位于 Android Monitor 中,該工具可以:
- 方便的顯示內(nèi)存使用和 GC 情況
- 快速定位卡頓是否和 GC 有關(guān)
- 快速定位 Crash 是否和內(nèi)存占用過(guò)高有關(guān)
- 快速定位潛在的內(nèi)存泄露問(wèn)題(內(nèi)存占用一直在增長(zhǎng))
- 但是不能準(zhǔn)確的定位問(wèn)題
Allocation Tracker
該工具用途:
- 可以定位代碼中分配的對(duì)象類型、大小、時(shí)間、線程、堆棧等信息
- 可以定位內(nèi)存抖動(dòng)問(wèn)題
- 配合 Heap Viewer 定位內(nèi)存泄露問(wèn)題(可以找出來(lái)泄露的對(duì)象是在哪創(chuàng)建的等等)
使用方法:在 Memory Monitor 中有個(gè) Start Allocation Tracking 按鈕即可開(kāi)始跟蹤 在點(diǎn)擊停止跟蹤后會(huì)顯示統(tǒng)計(jì)結(jié)果。
Heap Viewer
該工具用于:
- 顯示內(nèi)存快照信息
- 每次 GC 后收集一次信息
- 查找內(nèi)存泄露的利器
使用方法: 在 Memory Monitor 中有個(gè) Dump Java Heap 按鈕,點(diǎn)擊即可,在統(tǒng)計(jì)報(bào)告左上角選按 package 分類。配合 Memory Monitor 的 initiate GC(執(zhí)行 GC)按鈕,可檢測(cè)內(nèi)存泄露等情況。
LeakCanary
重要的事情說(shuō)三遍:
- for (int i = 0; i < 3; i++) {
- Log.e(TAG, "檢測(cè)內(nèi)存泄露的神器!");
- }
LeakCanary 具體使用不再贅述,自行 Google。