1. 程式人生 > >深入了解OOM

深入了解OOM

square mea 內存 後置 onmeasure 延遲加載 靜態成員 solution src

對於Java來說,內存泄漏就是new出來的對象放在Heap上無法被垃圾收集器回收,占據內存,導致內存越來越小進而程序崩潰。說到內存泄漏,不得不提另外一個易混淆的概念:內存溢出(OOM)。什麽是內存溢出呢?內存溢出是指程序向系統請求分配的空間超出了系統能給的。比如一輛車只能坐10個人,但是硬塞了15個人上去,就會擠爆了。大量的內存泄漏會導致內存溢出。

一、Java內存

說到內存,我們就來稍微了解下JVM內存。JVM內存分為method、stack、heap。

1)method:方法區,也叫靜態區,主要是存放一些靜態數據和常量,在編譯時就分配好,在程序整個運行期間都存在,被所有線程共享;

2)stack:棧區,主要存放基本數據類型和對象的引用(不是對象本身),遵循先進後出的原則,當方法執行時,會在棧區內存中創建方法體中的局部變量,方法執行完畢自動釋放內存;

3)heap:堆區,主要存放new出來的對象和數組,存儲的是對象本身。JVM中只有一個堆區被所有線程共享,由JVM的垃圾回收器自動管理。

二、內存泄漏分析

了解了Java的內存分配,那麽到底是哪裏的內存會導致內存泄漏呢?

在JAVA中JVM的棧記錄了方法的調用,每個線程擁有一個棧。在線程的運行過程當中,執行到一個新的方法調用,就在棧中增加一個內存單元,即幀(frame)。在frame中,保存有該方法調用的參數、局部變量和返回地址。然而JAVA中的局部變量只能是基本類型變量(int),或者對象的引用。所以在棧中只存放基本類型變量和對象的引用。引用的對象保存在堆中。

當某方法運行結束時,該方法對應的frame將會從棧中刪除,frame中所有局部變量和參數所占有的空間也隨之釋放。線程回到原方法繼續執行,當所有的棧都清空的時候,程序也就隨之運行結束。

而對於堆內存,堆存放著普通變量。在JAVA中堆內存不會隨著方法的結束而清空,所以在方法中定義了局部變量,在方法結束後變量依然存活在堆中。

綜上所述,棧(stack)可以自行清除不用的內存空間,Java的垃圾回收(簡稱GC)可以自動清空堆中不再使用的對象。在JAVA中對象是通過引用使用的。如果再沒有引用指向該對象,那麽該對象就無從處理或調用該對象,這樣的對象稱為不可到達,GC會將其回收,釋放其所占的內存空間。實現的思想如下:

1、棧中存放對象的引用,遍歷棧,根據引用找到堆中對應的對象;

2、因為棧中的對象的引用執行完畢就刪除了,所以根據棧中的引用,在堆中找不到的這些對象就是不可到達的,對其進行垃圾回收。

如果持有對象的強引用,那麽GC是沒有辦法在內存中將該對象釋放的,從而導致內存泄漏。那麽問題來了,其他的引用類型是否會被GC回收呢?

三、引用類型

1. 強引用(Strong reference)
實際編碼中最常見的一種引用類型。常見形式如:A a = new A();等。強引用本身存儲在棧內存中,其存儲指向堆內存中對象的地址。一般情況下,當對內存中的對象不再有任何強引用指向它時,垃圾回收機器開始考慮可能要對此內存進行的垃圾回收。如當進行編碼:a = null,此時,剛剛在堆中分配地址並新建的a對象沒有其他的任何引用,當系統進行垃圾回收時,堆內存將被垃圾回收。

2. 軟引用(Soft Reference)
軟引用的一般使用形式如下:

A a = new A();
SoftReference<A> srA = new SoftReference<A>(a);

軟引用所指示的對象進行垃圾回收需要滿足如下兩個條件:
1.當其指示的對象沒有任何強引用對象指向它;
2.當虛擬機內存不足時。
因此,SoftReference變相的延長了其指示對象占據堆內存的時間,直到虛擬機內存不足時垃圾回收器才回收此堆內存空間。

3. 弱引用(Weak Reference)
同樣的,軟引用的一般使用形式如下:

A a = new A();
WeakReference<A> wrA = new WeakReference<A>(a);

WeakReference不改變原有強引用對象的垃圾回收時機,一旦其指示對象沒有任何強引用對象時,此對象即進入正常的垃圾回收流程。

4. 虛引用(Phantom Reference)
與SoftReference或WeakReference相比,PhantomReference主要差別體現在如下幾點:
1.PhantomReference只有一個構造函數

PhantomReference(T referent, ReferenceQueue<? super T> q)

2.不管有無強引用指向PhantomReference的指示對象,PhantomReference的get()方法返回結果都是null。

因此,PhantomReference使用必須結合ReferenceQueue;
與WeakReference相同,PhantomReference並不會改變其指示對象的垃圾回收時機。

四、內存泄漏的場景

1)永遠的Singleton

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

 1 package com.example.nanchen.anrsolutiondemo.singleton;
 2 
 3 import android.content.Context;
 4 
 5 /**
 6  * @author nanchen
 7  * @fileName ANRSolutionDemo
 8  * @packageName com.example.nanchen.anrsolutiondemo.singleton
 9  * @date 2016/09/23  11:27
10  */
11 public class SingletonBad {
12     private static SingletonBad singletonBad;
13     private Context context;
14 
15     private SingletonBad(Context context){
16         this.context = context;
17     }
18 
19     public static SingletonBad getInstance(Context context){
20         if (singletonBad == null){
21             singletonBad = new SingletonBad(context);
22         }
23         return singletonBad;
24     }
25 }

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

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

 1 package com.example.nanchen.anrsolutiondemo.singleton;
 2 
 3 import android.content.Context;
 4 
 5 /**
 6  * @author nanchen
 7  * @fileName ANRSolutionDemo
 8  * @packageName com.example.nanchen.anrsolutiondemo.singleton
 9  * @date 2016/09/23  11:29
10  */
11 public class SingletonGood {
12     private static SingletonGood singletonGood;
13     private Context context;
14 
15     private SingletonGood(Context context){
16         this.context = context.getApplicationContext();//獲取Application的context避免內存泄漏
17     }
18 
19     public static SingletonGood getInstance(Context context){
20         if (singletonGood == null){
21             singletonGood = new SingletonGood(context);
22         }
23         return singletonGood;
24     }
25 }

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

 1 package com.example.nanchen.anrsolutiondemo.singleton;
 2 
 3 import android.app.Application;
 4 import android.content.Context;
 5 
 6 /**
 7  * @author nanchen
 8  * @fileName ANRSolutionDemo
 9  * @packageName com.example.nanchen.anrsolutiondemo.singleton
10  * @date 2016/09/23  11:32
11  */
12 public class DemoApplication extends Application {
13     private static DemoApplication demoApplication;
14 
15     public static DemoApplication getInstance(){
16         return demoApplication;
17     }
18 
19     public static Context getContext(){
20         return demoApplication.getApplicationContext();
21     }
22 
23     @Override
24     public void onCreate() {
25         super.onCreate();
26         demoApplication = this;
27     }
28 }

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

 1 package com.example.nanchen.anrsolutiondemo.singleton;
 2 
 3 import android.content.Context;
 4 
 5 /**
 6  * @author nanchen
 7  * @fileName ANRSolutionDemo
 8  * @packageName com.example.nanchen.anrsolutiondemo.singleton
 9  * @date 2016/09/23  11:32
10  */
11 public class SingletonGoodNew {
12     private static SingletonGoodNew singletonGoodNew;
13 
14     private Context context;
15     
16     private SingletonGoodNew(){
17         this.context = DemoApplication.getContext();
18     }
19 
20     public static SingletonGoodNew getInstance(){
21         if (singletonGoodNew == null){
22             singletonGoodNew = new SingletonGoodNew();
23         }
24         return singletonGoodNew;
25     }
26 }

2)令人心塞的Handler

這個東西在我最近遇到的最多了,而它也是我們在內存泄漏中最為常見的,也許你的一個小忽略就會導致內存泄漏。在android的新版本中,我們被要求必須把網絡任務等耗時操作置於新線程來處理,我們通常會采用Handler。但Handler不是萬能的,若是我們的編寫不規範就有可能會造成內存泄漏。另外,我們知道,Handler、Message和MessageQueue都是相互關聯在一起的,萬一Handler發送的Message尚未被處理,則該Message及發送它的Handler對象將會被線程MessageQueue一直持有。

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

 1 package com.example.nanchen.anrsolutiondemo.handler;
 2 
 3 import android.os.Bundle;
 4 import android.os.Handler;
 5 import android.os.Message;
 6 import android.support.v7.app.AppCompatActivity;
 7 
 8 import com.example.nanchen.anrsolutiondemo.R;
 9 
10 public class HandlerBadActivity extends AppCompatActivity {
11 
12     private final Handler handler = new Handler(){
13         @Override
14         public void handleMessage(Message msg) {
15             super.handleMessage(msg);
16         }
17     };
18 
19     @Override
20     protected void onCreate(Bundle savedInstanceState) {
21         super.onCreate(savedInstanceState);
22         setContentView(R.layout.activity_handler_bad);
23 
24         // 延遲10s發送一個消息
25         handler.postDelayed(new Runnable() {
26             @Override
27             public void run() {
28                 // write something
29             }
30         },1000*60*5);
31 
32         this.finish();
33     }
34 }

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

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

 1 package com.example.nanchen.anrsolutiondemo.handler;
 2 
 3 import android.os.Bundle;
 4 import android.os.Handler;
 5 import android.os.Message;
 6 import android.support.v7.app.AppCompatActivity;
 7 
 8 import com.example.nanchen.anrsolutiondemo.R;
 9 
10 import java.lang.ref.WeakReference;
11 
12 public class HandlerGoodActivity extends AppCompatActivity {
13 
14     private static final class MyHandler extends Handler{
15         private final WeakReference<HandlerGoodActivity> mActivity;
16 
17         public MyHandler(HandlerGoodActivity activity){
18             this.mActivity = new WeakReference<HandlerGoodActivity>(activity);//使用弱引用
19         }
20 
21         @Override
22         public void handleMessage(Message msg) {
23             super.handleMessage(msg);
24             HandlerGoodActivity activity = mActivity.get();
25             if (activity != null){
26                 // write something
27             }
28         }
29     }
30 
31     private final MyHandler myHandler = new MyHandler(this);
32 
33     // 匿名內部類在static的時候絕對不會持有外部類的引用
34     private static final Runnable RUNNABLE = new Runnable() {
35         @Override
36         public void run() {
37 
38         }
39     };
40 
41     @Override
42     protected void onCreate(Bundle savedInstanceState) {
43         super.onCreate(savedInstanceState);
44         setContentView(R.layout.activity_handler_good);
45 
46         myHandler.postDelayed(RUNNABLE,1000*60*5);
47     }
48 }

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

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

技術分享

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

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

 1 package com.example.nanchen.anrsolutiondemo.photo;
 2 
 3 import android.graphics.Bitmap;
 4 import android.graphics.BitmapFactory;
 5 
 6 import java.lang.ref.SoftReference;
 7 import java.util.HashMap;
 8 
 9 /**
10  * @author nanchen
11  * @fileName ANRSolutionDemo
12  * @packageName com.example.nanchen.anrsolutiondemo.photo
13  * @date 2016/09/23  11:58
14  */
15 public class CacheBySoftRef {
16 
17     // 首先定義一個HashMap,保存軟應用對象
18     private HashMap<String,SoftReference<Bitmap>> imageCache =  new HashMap<>();
19     // 再來定義一個方法,保存Bitmap的軟引用到HashMap
20     public void addBitmapToCache(String path){
21         // 強引用的Bitmap對象
22         Bitmap bitmap = BitmapFactory.decodeFile(path);
23         // 軟引用的Bitmap對象
24         SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
25         // 添加該對象到Map使其緩存
26         imageCache.put(path,softBitmap);
27     }
28 
29     // 獲取的時候,可以通過SoftReference的get()的方法得到Bitmap對象
30     public Bitmap getBitmapByPath(String path){
31         // 從緩存中取軟引用的Bitmap對象
32         SoftReference<Bitmap> softBitmap = imageCache.get(path);
33         // 判斷是否存在軟引用
34         if (softBitmap == null){
35             return null;
36         }
37         // 通過軟引用取出Bitmap對象,如果由於內存不足Bitmap被回收,則取得空;
38         // 如果未被回收,則可重復使用,提高速度
39         Bitmap bitmap = softBitmap.get();
40         return bitmap;
41     }
42 }

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

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

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

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

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

技術分享

3)匿名內部類/非靜態內部類,它們方便卻暗藏殺機

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

 1 package com.example.nanchen.anrsolutiondemo.innerClass;
 2 
 3 import android.os.Bundle;
 4 import android.support.v7.app.AppCompatActivity;
 5 
 6 import com.example.nanchen.anrsolutiondemo.R;
 7 
 8 public class MainActivity extends AppCompatActivity {
 9 
10     private static TestInnerBad testInnerBad = null;
11 
12     class TestInnerBad{
13 
14     }
15 
16     @Override
17     protected void onCreate(Bundle savedInstanceState) {
18         super.onCreate(savedInstanceState);
19         setContentView(R.layout.activity_inner_bad);
20 
21         if (testInnerBad == null){
22             testInnerBad = new TestInnerBad();
23         }
24 
25         Runnable runnable1 = new MyRunnable();
26         Runnable runnable2 = new Runnable() {
27             @Override
28             public void run() {
29 
30             }
31         };
32     }
33 
34     private static class MyRunnable implements Runnable{
35 
36         @Override
37         public void run() {
38 
39         }
40     }
41 
42 }

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

技術分享

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

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

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

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

這裏修復的方法是:

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

5)遠離非靜態內部類和匿名類,多用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 中才能創建

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

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

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

 1 private List<String> nameList;
 2     private List<Fragment> list;
 3 
 4     @Override
 5     public void onDestroy() {
 6         super.onDestroy();
 7         if (nameList != null){
 8             nameList.clear();
 9             nameList = null;
10         }
11         if (list != null){
12             list.clear();
13             list = null;
14         }
15     }

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

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

  解決方案:

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

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

8)做一個小的總結

  • 構造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,這樣的設計誰都得不到釋放。

五、內存泄漏檢測工具

MAT(Memory Analyzer Tool):點我下載~

強大的開源內存檢測工具LeakCanary:官方文檔

參考鏈接:

Android內存泄漏的八種可能(上)

[譯]Android防止內存泄漏的八種方法(下)

深入了解OOM