內存泄漏(Memory Leak)是指程序中己動態分配的堆內存因爲某種緣由程序未釋放或沒法釋放,形成系統內存的浪費,致使程序運行速度減慢甚至系統崩潰等嚴重後果。有些內存泄漏是很難發現的,須要使用恰當的方法或者輔助工具才能檢測到,這篇文章記一下 Android 應用程序中如何檢測內存泄漏。java
java 虛擬機在執行 java 程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域,這些區域有着各自的建立時間和銷燬時間,各自用途也不同。java虛擬機運行時數據區域以下圖:
android
程序計數器是一塊較小的內存空間,是當前線程所執行的字節碼的行號指示器。若是線程正在執行一個java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是是Naive方法,則計數器爲空;這個區域不會出現OUtOfMemoryError異常。此區域也是惟一一個在java虛擬機規範中沒有規定任何OUtOfMemoryError狀況的區域。
java虛擬機多線程是使用線程輪流切換並分配處理執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器都只會執行一條線程中的指令。爲了線程切換後可以恢復到正確的執行位置,每條線程都須要一套獨立的線程計數器,這些計數器之間相互獨立,獨立存儲,這個內存區域爲「線程私有」。程序員
java虛擬機棧也是線程私有,與線程的生命週期一致,在執行每一個方法都會建立一個Stack Frame。每個方法從開始執行到結束,對應一個Stack Frame在虛擬機值棧中從入棧和出棧的過程。若是線程請求的棧深度大於虛擬機所容許的深度,就會出現StackOverFlowException。若是容許動態擴展,在擴展的過程當中,若是沒法申請到足夠的內存,則會拋出OutOfMemoryException異常。數組
和java虛擬機棧的做用相似,不一樣點在本地方法棧主要是爲虛擬機使用到的Native方法提供服務,本地方法棧也會拋出StackOverFlowException和OutOfMemoryException異常。緩存
堆是java虛擬機中內存中最大的一塊,被全部線程共享的一塊內存區域,在虛擬機建立時建立。做用就是存放對象實例,全部的對象的實例都須要在這裏分配內存。幾乎全部的對象實例和對象數組都須要在堆上分配。是java虛擬機內存回收的管理的重要區域,所以也被稱爲「GC」堆,能夠被分爲:新生代和老年代;Eden空間、From Survivor空間、To Survivor空間。若是堆中沒有內存完成實例分配,而且堆也沒法擴展時,則拋出OutOfMemoryException異常。網絡
方法區和java堆同樣,是各個線程共享的內存區域,用於存儲被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯的代碼等數據。一般被開發人員成爲「永久帶」。這個區域的內存回收的目標就是針對常亮池的回收和對類型的卸載,也是較爲難處理的部分。若是方法區的內存空間不知足內存分配需求時,Java虛擬機會拋出OutOfMemoryError異常多線程
運行時常量池(Runtime Constant Pool)是方法區的一部分。它用來存放編譯時期生成的字面量和符號引用,這些內容會在類加載後存放在方法區的運行時常量池中。運行時常量池能夠理解爲是類或接口的常量池的運行時表現形式。當建立類或接口時,若是構造運行時常量池所需的內存超過了方法區所能提供的最大值,Java虛擬機會拋出OutOfMemoryError異常。app
java 堆中存儲的都是引用類型的數據,而 java 對象存在四種引用類型,分別是強引用、軟引用、弱引用和虛引用。
Java中提供這四種引用類型主要有兩個目的:
第一是可讓程序員經過代碼的方式決定某些對象的生命週期;
第二是有利於JVM進行垃圾回收。框架
下面來闡述一下這四種類型引用的概念:eclipse
強引用類型是咱們平時寫代碼的時候最經常使用的引用,指建立一個對象並把這個對象賦給一個引用變量。剛學習java的人都會忽略這個概念,成一種理所固然的事情了,好比:
Class Apple{} Apple apple =new Apple();
若是某個對象被強引用所引用,這個引用又被其餘對象所持有,那麼JVM 寧願拋出 OutOfMemory 錯誤也不會回收這種對象。
若是一個對象具備軟引用,內存空間足夠,垃圾回收器就不會回收它;
若是內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就能夠被程序使用。
軟引用可用來實現內存敏感的高速緩存,好比網頁緩存、圖片緩存等。使用軟引用能防止內存泄露,加強程序的健壯性。
SoftReference的特色是它的一個實例保存對一個Java對象的軟引用, 該軟引用的存在不妨礙垃圾收集線程對該Java對象的回收。
也就是說,一旦SoftReference保存了對一個Java對象的軟引用後,在垃圾線程對 這個Java對象回收前,SoftReference類所提供的get()方法返回Java對象的強引用。
另外,一旦垃圾線程回收該Java對象之 後,get()方法將返回null。
舉個例子:
Class Apple{} Apple apple = new Apple(); SoftReference<Apple> softRef = new SoftReference<Apple>(apple);
此時這個 Apple 對象有兩個引用,一個是強引用 apple
,一個是軟引用 softRef
,若想該對象只被軟引用引用,只需將強引用和該對象的鏈接斷開。
apple = null;
在該對象被回收以前,咱們也可經過 get
方法得到該對象,再聲明一個強引用指向它便可:
Apple a = softRef.get();
此時 a 便是該對象的強引用。若是該對象已經被回收,那麼 softRef.get()
將返回 null。
SoftReference 有兩個構造方法:
public SoftReference(T referent) { super(referent); this.timestamp = clock; } public SoftReference(T referent, ReferenceQueue<? super T> q) { }
做爲一個Java對象,SoftReference對象除了具備保存軟引用的特殊性以外,也具備Java對象的通常性。因此,當軟可及對象被回收以後,雖然這個SoftReference對象的get()方法返回null,但這個SoftReference對象已經再也不具備存在的價值,須要一個適當的清除機制,避免大量SoftReference對象帶來的內存泄漏。在java.lang.ref包裏還提供了ReferenceQueue。若是在建立SoftReference對象的時候,使用了一個ReferenceQueue對象做爲參數提供給SoftReference的構造方法,那麼當這個SoftReference所軟引用的aMyOhject被垃圾收集器回收的同時,ref所強引用的SoftReference對象被列入ReferenceQueue。也就是說,ReferenceQueue中保存的對象是Reference對象,並且是已經失去了它所軟引用的對象的 Reference 對象。另外從 ReferenceQueue 這個名字也能夠看出,它是一個隊列,當咱們調用它的poll()方法的時候,若是這個隊列中不是空隊列,那麼將返回隊列前面的那個Reference對象。
在任什麼時候候,咱們均可以調用ReferenceQueue的poll()方法來檢查是否有它所關心的非強可及對象被回收。若是隊列爲空,將返回一個null,不然該方法返回隊列中前面的一個Reference對象。利用這個方法,咱們能夠檢查哪一個SoftReference所軟引用的對象已經被回收。因而咱們能夠把這些失去所軟引用的對象的SoftReference對象清除掉。
弱引用,就是引用與對象之間的聯繫很弱,弱到垃圾回收器會無視這個引用,直接回收對象。
弱引用與軟引用的區別在於:只具備弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。不過,因爲垃圾回收器是一個優先級很低的線程,所以不必定會很快發現那些只具備弱引用的對象。
弱引用也能夠和一個引用隊列(ReferenceQueue)聯合使用,若是弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。
Class Apple{} Apple apple = new Apple(); WeakReference<Apple> weakRef = new WeakReference<Apple>(apple); apple = null;
「虛引用」顧名思義,就是形同虛設,與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收器回收。虛引用主要用來跟蹤對象被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用隊列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會在回收對象的內存以前,把這個虛引用加入到與之關聯的引用隊列中。
通常 java 中所說的內存泄漏指的是 java 堆中所產生的內存泄漏,java 堆中內存是由 java 虛擬機的「GC」所管理的,java 虛擬機實時監控到一些對象不可到達,即在之後的程序中不會被用到,觸發 GC 時,這樣的對象將會被回收掉,其所佔的內存也將釋放。從對象的生命週期角度來說,若是一個長生命週期的對象持有了一個短生命週期對象的引用,將致使這個短生命週期的對象沒法回收,其所佔的內存釋放不了,就會有內存泄漏的風險。
Android 中使用 Handler 形成內存泄漏的代碼:
public class MainActivity extends AppCompatActivity { private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { // ... } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // ... } }
上面是一段簡單的Handler的使用。當使用內部類(包括匿名類)來建立Handler的時候,Handler對象會隱式地持有一個外部類對象(一般是一個Activity)的引用(否則你怎麼可能經過Handler來操做Activity中的View?)
而Handler一般會伴隨着一個耗時的後臺線程(例如從網絡拉取圖片)一塊兒出現,這個後臺線程在任務執行完畢(例如圖片下載完畢)以後,經過消息機制通知Handler,而後Handler把圖片更新到界面。然而,若是用戶在網絡請求過程當中關閉了Activity,正常狀況下,Activity再也不被使用,它就有可能在GC檢查時被回收掉,但因爲這時線程還沒有執行完,而該線程持有Handler的引用(否則它怎麼發消息給Handler?),這個Handler又持有Activity的引用,就致使該Activity沒法被回收(即內存泄露),直到網絡請求結束(例如圖片下載完畢)。另外,若是你執行了Handler的postDelayed()方法,該方法會將你的Handler裝入一個Message,並把這條Message推到MessageQueue中,那麼在你設定的delay到達以前,會有一條MessageQueue -> Message -> Handler -> Activity的鏈,致使你的Activity被持有引用而沒法被回收。
LeakCanary 開發者應該都會使用,不熟悉的新同窗能夠參考 LeakCanary 中文使用說明
使用內存分析器來執行如下操做:
如上圖所示,內存分析器的默認視圖包括如下內容:
① 強制執行垃圾收集事件的按鈕。
② 捕獲堆轉儲的按鈕。
③ 記錄內存分配的按鈕。
④ 放大時間線的按鈕。
⑤ 跳轉到實時內存數據的按鈕。
⑥ 事件時間線顯示活動狀態、用戶輸入事件和屏幕旋轉事件。
⑦ 內存使用時間表,其中包括如下內容:
查看堆轉儲時,查看分配了多少內存的快照頗有用,它不會顯示如何分配內存。爲此,您須要記錄內存分配。完成記錄會話後,您能夠看到如下記錄的持續時間:
分配了哪些對象以及它們使用了多少空間。
在堆棧跟蹤中分配每一個對象的位置,其中包括線程。
要查看應用程序的內存分配,請單擊內存分析器工具欄中的Record memory allocations。當它記錄時,與你的應用程序進行交互,以引發內存溢出或內存泄漏。完成後,單擊Stop recording。
要檢查分配記錄,請按照下列步驟操做:
默認狀況下,列表是按類名排列的。在列表的頂部,您可使用右下拉菜單在列表之間切換:
要捕獲堆轉儲,單擊Memory-Profiler工具欄中的dump Java堆,而後分析結合上面選項分析,針對上述示例,結果以下圖:
分析結果已經很明確了。
上面示例比較簡單,針對複雜的問題,咱們還可使用 eclipse-MAT 工具加以分析。
根據前面講了關於軟引用和弱引用相關的基礎知識,那麼到底如何利用它們來優化程序性能,從而避免OOM的問題呢?
下面舉個例子,假若有一個應用須要讀取大量的本地圖片,若是每次讀取圖片都從硬盤讀取,則會嚴重影響性能,可是若是所有加載到內存當中,又有可能形成內存溢出,此時使用軟引用能夠解決這個問題。
設計思路是:用一個HashMap來保存圖片的路徑和相應圖片對象關聯的軟引用之間的映射關係,在內存不足時,JVM會自動回收這些緩存圖片對象所佔用的空間,從而有效地避免了OOM的問題。在Android開發中對於大量圖片下載會常常用到。
針對上述例子,首先咱們把匿名內部類 Handler 聲明爲靜態內部類,這樣Handler類中就不持有 Activity 的引用了。可是,這樣在 Handler 中要使用 activity 對象,則經過弱引用來解決這個問題。
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // ... } private static class NoLeakHandler extends Handler { //持有弱引用MainActivity,GC回收時會被回收掉. private WeakReference<MainActivity> mActivity; public NoLeakHandler(MainActivity activity) { mActivity = new WeakReference<>(activity); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); } } }
固然針對 Handler 的內存泄漏,咱們也能夠經過程序來解決:
@Override public void onDestroy() { // 移除全部消息 handler.removeCallbacksAndMessages(null); // 或者移除單條消息 handler.removeMessages(what); }