Android 內存泄露總結

Android 內存泄露總結

簡單的講就是,該被釋放的對象沒有被釋放,一直被某個或某些實例所持有卻不再被使用導致GC不能回收。

JAVA 內存分配策略

Java程序運行時的內存分配策略有3種:
- 靜態分配
- 棧式分配
- 堆式分配

三種存儲策略使用的內存空間分別是:

  • 靜態存儲區

    主要存放靜態數據全局static數據常量,這塊內存在程序編譯時就已分配好,並且在程序整個運行期間都存在。

  • 棧區

    當方法被執行時,方法體內的局部變量都在棧上創建,並在方法執行結束時,這些局部變量所持有的內存將會自動釋放。

  • 堆區

    又稱動態內存分配,通常指在程序運行時直接new出來的內存,這部分內存在不使用時將會由java垃圾回收器來負責回收。

棧與堆的區別

在方法體內定義的局部變量,一些基本類型的變量和對象的引用變量都是在方法的棧內存中分配的,當在一段方法塊中定義一個變量時,Java就會在棧中爲該變量分配內存空間,當超過該變量的作用域後,該變量也就無效了,分配給他的內存空間也將被釋放掉,該內存空間可以被重新使用。

堆內存用來存放所有由new創建的對象 ( 包括該對象其中的所有成員變量 ) 和數組。在堆中分配的內存,將由Java垃圾回收器來自動管理,在堆中產生了一個數組或者對象後,還可以在棧中定義一個特殊的變量,這個變量的取值等於數組或者對象在堆內存中的首地址,這個特殊的變量就是我們上面說的引用變量,我們可以通過這個引用變量來訪問堆中的對象和數組。

舉個例子:

public class Sample {

    public void method() {
        int s2 = 1;
        Sample mSample2 = new Sample();
    }
}

Sample 類的局部變量 s2 和 引用變量 mSample2 都是存在於棧中,但mSample2指向的對象是存放於堆上的;

結論


局部變量的基本數據類型引用存儲於棧中,引用的對象實體存儲於堆中。
—— 因爲它們屬於方法中的變量,生命週期隨方法而結束。

成員變量全部存儲於堆中 ( 包括基本數據類型引用引用的對象實體 )
—— 因爲它們屬於類,類對象終究是要被new出來使用的。

Java 是如何管理內存

Java的內存管理就是對象的分配和釋放問題。在Java中,程序員需要通過關鍵字new爲每個對象申請空間,所有的對象都在堆(Heap)中分配空間。另外,對象的釋放是由GC決定和執行的。在Java中,內存的分配是由程序完成的,而內存的釋放是由GC完成的。簡化了程序員的工作,加重了JVM的工作,這也是Java運行速度較慢的原因,因爲GC爲了能夠正確釋放對象,GC必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC都需要監控。

爲了更好理解GC的工作原理,我們可以考慮將對象考慮爲有向圖的頂點,將引用關係考慮爲有向邊,有向邊從引用者指向被引對象,另外,每個線程對象可以作爲一個圖的起始頂點,例如大多數程序從main進程開始執行,那麼該圖就是以main進程頂點開始的一顆樹。在這個有向圖中,根頂點可達的對象都是有效對象,GC將不回收這些對象,反之,不可達則將被GC回收。

什麼是Java中的內存泄露

在Java中,內存泄露就是存在一些被分配的對象,這些對象有下面兩個特點:
- 這些對象是可達的,即在有向圖中,存在通路可以與其相連;
- 這些對象都是無用的,即程序以後不會再使用這些對象;

如果對象滿足這2個條件,這些對象就可以判定爲Java中的內存泄露,這些對象不會被GC回收,然而它卻佔用內存。

對於我們來說,GC基本是透明的,不可見的,可運行GC的函數 System.gc(),但該函數不保證JVM的垃圾收集器一定會執行。因爲不同JVM實現者可能使用不同的算法管理GC,通常,GC的線程優先級較低,JVM調用GC的策略也有很多種,有的內存使用達到一定程度,GC纔開始工作,也有定時執行的。有的平緩執行GC,有的中斷式執行GC。GC的執行影響應用程序的性能。例如對於基於Web的實時系統,如網絡遊戲等,用戶不希望GC突然中斷應用程序執行而進行垃圾回收,那麼我們需要調整GC的參數,讓GC能夠通過平緩的方式釋放內存,例如將垃圾回收分解爲一系列小步驟執行:
給出一個內存泄露的例子:

List v = new ArrayList(10);
for (int i = 1; i < 10; i++) {
    Object o = new Object();
    v.add(o);
    o = null;     
}

在這個例子中,我們循環申請Object對象,並將所申請的對象放入一個List中,如果我們僅僅釋放引用本身,那麼List仍然引用該對象,所以這個對象對GC來說是不可回收的,因此,如果對象加入到List後,還需要從List中刪除,最簡單的方式就是將List對象設置爲NULL;

Android中常見的內存泄露彙總

  • 集合類泄露

集合類如果僅僅有添加元素的方法,而沒有相應的刪除機制,導致內存被佔用。如果這個集合類是全局變量,比如類中的靜態屬性,全局性的Map等即有靜態引用或final一直指向它,那麼沒有相應的刪除機制,很可能導致集合所佔用的內存只增不減;

  • 單例造成的內存泄露

由於單例的靜態特性使得其生命週期跟應用的生命週期一樣長,所以如果使用不恰當的話,很容易造成內存泄露;

比如下面一個典型例子:

public class AppManager {
    private static AppManager instance;
    private Context context;

    private AppManager(Context context) {
        this.context = context;
    }

    public static AppManager getInstance(Context context){
        if(instance == null){
            synchronized(AppManager.class) {
                if(instance == null) {
                    instance = new AppManager(context);
                }
            }
        }
        return instance;
    }

}

這是一個普通的單例模式,當創建這個單例的時候,需要傳入一個Context,所以這個Context的生命週期的長度至關重要:
- 如果此時傳入的是Application的Context,因爲Application的生命週期就是整個應用的生命週期,所以沒有問題;

  • 如果此時傳入的是Activity的Context,當這個Context所對應的Activity退出時,由於該Context的引用被單例對象所持有,其生命週期等於整個應用的生命週期,所以當前Activity退出時它的內存並不會被回收,這就造成內存泄露。

正確的方式應該改爲下面格式:

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context){
        //使用Application的context
        this.context = context.getApplicationContext();
    }

    public static AppManager getInstance(Context context){
        if(instance == null) {
            synchronized(AppManager.class) {
                if(instance == null) {
                    instance = new AppManager(context);
                }
            }
        }
        return instance;
    }
}

或者在Application中添加一個靜態方法 getContext() 返回ApplicatonContext

  • 非靜態內部類創建靜態實例造成的內存泄露

有的時候我們可能會在啓動頻繁的Activity中,爲了避免重複創建相同的數據資源,可能會出現這種寫法:

public class MainActivty extends AppCompatActivity {
    private static TestResource mResource = null;

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if(mResource == null){
            mResouce = new TestResource();
        }

        //...
    }

    class TestResource{
        //...
    }
}

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

正確的做法是;

將該內部類設爲靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,推薦使用Application的Context,當然Application的Context不是萬能的,所以也不能隨便使用,對於有些地方則必須使用Activity的Context;

  • 匿名內部類

android開發經常會繼承實現ActivityFragmentView;此時你使用了匿名類,並被異步線程持有了,那要小心了,如果沒有任何措施這樣一定會導致內存泄露;

public class MainActivity extends Activity {
    ...

    Runnable ref1 = new MyRunnable();
    Runnable ref2 = new Runnable(){
        @Override
        public void run(){

        }
    }

    ...
}

ref1ref2 的區別是,ref2使用了匿名內部類,引用內存結構:

ref1 = MyRunnable (id = 830042475848)
ref2 = MainActivity$1 (id = 83004247656)
    this$0 = MainActivity (id = 830042474408)

可以看到,ref1沒什麼特別的;
ref2這個匿名類的實現對象裏面多了個引用;
this$0 這個引用指向MainActivity.this, 也就是說當前MainActivity實例會被ref2持有,如果將這個引用再傳入一個異步線程,此線程和Activity生命週期不一致的時候,就造成Activity的泄露。

  • Handler 造成的內存泄露

Handler的使用造成的泄露最爲常見,平時處理網絡任務或封裝一些請求回調等api都應該會藉助Handler來處理,對於Handler的使用代碼編寫一不規範即有可能造成內存泄露;如下示例:

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg){
            //...
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }

    private void loadData(){
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

這種創建Handler的方式會造成內存泄露,由於mHandlerHandler的非靜態匿名內部類的實例,所以它持有外部類Activity的引用,我們知道消息隊列是在一個Looper線程中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或正在處理的消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,所以導致Activity的內存資源無法及時回收,引發內存泄露,所以另一種做法:

public class MainActivity extends AppCopatActivity {
    private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView;
    private static class MyHandler extends Handler{
        private WeakReference reference;
        public MyHandler(Context context){
            reference = new WeakReference<>(context);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity)reference.get();
            if(activity != null){
                activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }

    private void loadData(){
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }

    @Override
    protected void onDestroy(){
        super.onDestroy();
        mHandler.removeCallbackAndMessages(null);

    }
}

創建一個靜態Handler內部類,然後對Handler持有的對象使用弱引用,這樣在回收時也可以回收Handler持有的對象,這樣雖然避免了Activity泄露,不過Looper線程的消息隊列中還是可能會有待處理的消息,所以我們在ActivityonDestroy或者onStop方法時移除消息隊列中的消息。

使用mHandler.removeCallbackAndMessages(null)是移除消息隊列中的所有消息和所有Runnagle

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

public final void removeCallbacks(Runnable r);
public final void removeCallbacks(Runnable r,Object token);
public final void removeCallbacksAndMessages(Object token);
public final void removeMessages(int what);
public final void removeMessages(int what,Object object);

  • 線程造成的內存泄露

對於線程造成的內存泄露,也是比較常見的,異步任務和Runnable都是一個匿名內部類,因此它們對當前Activity都有一個隱式引用,如果Activity在銷燬之前,任務還未完成,那麼將導致Activity的內存資源無法回收,造成內存泄露,正確的做法還是使用靜態內部類的方式。如下:

static class MyAsyncTask extends AsyncTask {
    private WeakReference weakReference;

    public MyAsyncTask(Context context){
        weakReference = new WeakReference<>(context);
    }

    @Override
    protected Void doInBackground(Void... params){
        SystemClock.sleep(10000);
        return null;
    }

    @Override
    protected void onPostExecute(Void aVoid){
        super.onPostExecute(aVoid);
        MainActivity activity = (MainActivity)weakReference.get();
        if(activity != null){
            //...
        }
    }
}

static class MyRunnable implements Runnable{
    @Override
    public void run(){
        SystemClock.sleep(10000);
    }
}

//----------
new Thread(new MyRunnagle()).start();
new MyAsyncTask(this).execute();

這樣就避免了Activity的內存資源泄露,當然在Activity銷燬時候也應該取消相應的任務AsyncTask :: cancel(),避免任務在後臺執行浪費資源;

  • 儘量避免使用static成員變量

如果成員變量被聲明爲static,那麼我們都知道其生命週期將與整個app進程週期一樣。

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

這裏修復的方法是:

不要在類初始時化靜態成員,可以考慮Lazy初始化。

  • 資源未關閉造成的內存泄露

對於使用了BraodcastReceiverContentObserverFile遊標CursorStreamBitmap等資源的使用,應該在Activity銷燬時及時關閉或者註銷,否則這些資源將不會被回收,造成內存泄露。

  • 一些不良代碼造成的內存壓力

有些代碼並不造成內存泄露,但是它們,或是對沒使用的內存沒進行有效及時的釋放,或是沒有有效的利用已有的對象而是頻繁的申請新內存。比如:

Bitmap沒調用 recycle()方法,對於Bitmap對象在不使用時,我們應該先調用recycle()釋放內存,然後將它設置爲null;

構造Adapter時,麼有使用緩存convertView,每次都在創建新的convertView,這裏推薦使用ViewHolder;

工具分析

使用LeakCanary檢測Android的內存泄露

LeakCanary 中文使用說明

Demo接入

build.gradle中加入引用,不同的編譯使用不同的引用;

dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}

如何使用

使用RefWatcher監控那些本該被回收的對象。

RefWatcher refWatcher = {...}
//監控
refWatcher.watch(schrodingerCat);

LeakCanary.install()會返回一個預定義的RefWatcher,同時也會啓用一個ActivityRefWatcher,用於自動監控調用Activity.onDestroy()之後泄露的Activity.

Application中進行配置:

public class ExampleApplication extends Application{
    private RefWatcher refWatcher;

    public static RefWatcher getRefWatcher(Context context){
        ExampleApplication application = (ExampleApplication)context.getApplicationContext();
        return application.refWatcher;
    }

    @Override
    public void onCreate(){
        super.onCreate();
        refWatcher = LeakCanary.install(this);

    }
}

使用RefWatcher 監控Fragment

public abstract class BaseFragment extends Fragment{
    @Ovirride
    public void onDestroy(){
        super.onDestroy();
        RefWatcher refWatcher = Examplepplication.getRefWatcher(getActivity());
        refWatcher.watch(this);
    }
}

使用RefWatcher監控Activity:

public class MainActivity extends AppCompatActivity{
    .....
    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //在自己的應用初始Activity中加入如下兩行代碼
        RefWatcher refWatcher = ExampleApplication.getRefWatcher(this);
        refWatcher.watch(this);
    }
}

Android Monitor Memory

Android Studio 自帶的內存監視,可觀察應用內存佔用,運行應用一段時間如果內存佔用持續升高,有可能存在內存泄露。

Android Device Monitor

SDK 的 Device Monitor是分析應用內存分配情況的好工具。

  • Heap
    可查看堆內存,使用:選中進程,點擊update heap,點擊Cause GC即可顯示該進程的內存情況,以後每次GC都會更新,也可手動Cause GC

總結

  • Activity等組件的引用應該控制在Activity的生命週期之內,如果不能就考慮使用getApplicationContext或者getApplication,以避免Activity被外部長生命週期的對象引用而泄露。
  • 儘量不要在靜態變量或者靜態內部類中使用非靜態成員變量(包括context),即使要使用,也要考慮適時把外部成員變量置空,也可以在內部類中使用弱引用來引用外部類的便量;
  • 對於生命週期比Activity長的內部類對象,並且內部類中使用了外部類的成員變量,可以這樣做避免內存泄露
    • 將內部類改爲靜態內部類
    • 靜態內部類中使用弱引用來引用外部類的成員變量
  • Handler的持有的引用對象最好使用弱引用,資源釋放時也可以清空Handler裏面的消息,比如在Activity onStop 或 onDestroy的時候,取消掉該Handler對象的MessageRunnable;
  • Java的實現過程中,也要考慮其對象釋放,最好的方法時在不使用某對象時,顯示的將此對象置爲null,比如使用完Bitmap後先調用recycle(),再賦值爲null,清空對圖片等資源有直接引用或者間接引用的數組(使用array.clear();array = null)等,最好遵循誰創建誰釋放原則。
  • 正確關閉資源,對於使用了BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或註銷。
  • 保持對對象生命週期的敏感,特別注意單例,靜態對象,全局性集合等的生命週期。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章