1. 程式人生 > >Android記憶體介紹及記憶體洩漏

Android記憶體介紹及記憶體洩漏

Java 記憶體分配策略

Java 程式執行時的記憶體分配策略有三種,分別是靜態分配,棧式分配,和堆式分配,對應的,三種儲存策略使用的記憶體空間主要分別是靜態儲存區(也稱方法區)、棧區和堆區。

靜態儲存區(方法區):主要存放靜態資料、全域性 static 資料和常量。這塊記憶體在程式編譯時就已經分配好,並且在程式整個執行期間都存在。

棧區 :當方法被執行時,方法體內的區域性變數都在棧上建立,並在方法執行結束時這些區域性變數所持有的記憶體將會自動被釋放。因為棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限。

堆區 : 又稱動態記憶體分配,通常就是指在程式執行時直接 new 出來的記憶體。這部分記憶體在不使用時將會由 Java 垃圾回收器來負責回收。

在方法體內定義的(區域性變數)一些基本型別的變數和物件的引用變數都是在方法的棧記憶體中分配的。當在一段方法塊中定義一個變數時,Java 就會在棧中為該變數分配記憶體空間,當超過該變數的作用域後,該變數也就無效了,分配給它的記憶體空間也將被釋放掉,該記憶體空間可以被重新使用。

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

結論:

區域性變數的基本資料型別和引用儲存於棧中,引用的物件實體儲存於堆中。—— 因為它們屬於方法中的變數,生命週期隨方法而結束。

成員變數全部儲存與堆中(包括基本資料型別,引用和引用的物件實體)—— 因為它們屬於類,類物件終究是要被new出來使用的。

瞭解了 Java 的記憶體分配之後,我們再來看看 Java 是怎麼管理記憶體的。

Java的記憶體管理就是物件的分配和釋放問題。在 Java 中,程式設計師需要通過關鍵字 new 為每個物件申請記憶體空間 (基本型別除外),所有的物件都在堆 (Heap)中分配空間。另外,物件的釋放是由 GC 決定和執行的。在 Java 中,記憶體的分配是由程式完成的,而記憶體的釋放是由 GC 完成的,這種收支兩條線的方法確實簡化了程式設計師的工作。但同時,它也加重了JVM的工作。這也是 Java 程式執行速度較慢的原因之一。因為,GC 為了能夠正確釋放物件,GC 必須監控每一個物件的執行狀態,包括物件的申請、引用、被引用、賦值等,GC 都需要進行監控。

監視物件狀態是為了更加準確地、及時地釋放物件,而釋放物件的根本原則就是該物件不再被引用。

Java中的記憶體洩漏

1.Java記憶體回收機制

不論哪種語言的記憶體分配方式,都需要返回所分配記憶體的真實地址,也就是返回一個指標到記憶體塊的首地址。Java中物件是採用new或者反射的方法建立的,這些物件的建立都是在堆(Heap)中分配的,所有物件的回收都是由Java虛擬機器通過垃圾回收機制完成的。GC為了能夠正確釋放物件,會監控每個物件的執行狀況,對他們的申請、引用、被引用、賦值等狀況進行監控,Java會使用有向圖的方法進行管理記憶體,實時監控物件是否可以達到,如果不可到達,則就將其回收,這樣也可以消除引用迴圈的問題。在Java語言中,判斷一個記憶體空間是否符合垃圾收集標準有兩個:一個是給物件賦予了空值null,以下再沒有呼叫過,另一個是給物件賦予了新值,這樣重新分配了記憶體空間。

2.Java記憶體洩漏引起的原因

記憶體洩漏是指無用物件(不再使用的物件)持續佔有記憶體或無用物件的記憶體得不到及時釋放,從而造成記憶體空間的浪費稱為記憶體洩漏。記憶體洩露有時不嚴重且不易察覺,這樣開發者就不知道存在記憶體洩露,但有時也會很嚴重,會提示你Out of memory。

Java記憶體洩漏的根本原因是什麼呢?長生命週期的物件持有短生命週期物件的引用就很可能發生記憶體洩漏,儘管短生命週期物件已經不再需要,但是因為長生命週期持有它的引用而導致不能被回收,這就是Java中記憶體洩漏的發生場景。具體主要有如下幾大類:

Static Activities

在類中定義了靜態Activity變數,把當前執行的Activity例項賦值於這個靜態變數。
如果這個靜態變數在Activity生命週期結束後沒有清空,就導致記憶體洩漏。因為static變數是貫穿這個應用的生命週期的,所以被洩漏的Activity就會一直存在於應用的程序中,不會被垃圾回收器回收。

     static Activity activity;

    void setStaticActivity() {
      activity = this;
    }

    View saButton = findViewById(R.id.sa_button);
    saButton.setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
        setStaticActivity();
        nextActivity();
      }
    });

Memory Leak 1 - Static Activity

Static Views

類似的情況會發生在單例模式中,如果Activity經常被用到,那麼在記憶體中儲存一個例項是很實用的。正如之前所述,強制延長Activity的生命週期是相當危險而且不必要的,無論如何都不能這樣做。

特殊情況:如果一個View初始化耗費大量資源,而且在一個Activity生命週期內保持不變,那可以把它變成static,載入到檢視樹上(View Hierachy),像這樣,當Activity被銷燬時,應當釋放資源。(譯者注:示例程式碼中並沒有釋放記憶體,把這個static view置null即可,但是還是不建議用這個static view的方法)

    static view;

    void setStaticView() {
      view = findViewById(R.id.sv_button);
    }

    View svButton = findViewById(R.id.sv_button);
    svButton.setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
        setStaticView();
        nextActivity();
      }
    });

Memory Leak 2 - Static View

Inner Classes

繼續,假設Activity中有個內部類,這樣做可以提高可讀性和封裝性。將如我們建立一個內部類,而且持有一個靜態變數的引用,恭喜,記憶體洩漏就離你不遠了(譯者注:銷燬的時候置空,嗯)。

       private static Object inner;

       void createInnerClass() {
        class InnerClass {
        }
        inner = new InnerClass();
    }

    View icButton = findViewById(R.id.ic_button);
    icButton.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            createInnerClass();
            nextActivity();
        }
    });

Memory Leak 3 - Inner Class

內部類的優勢之一就是可以訪問外部類,不幸的是,導致記憶體洩漏的原因,就是內部類持有外部類例項的強引用。

Anonymous Classes

相似地,匿名類也維護了外部類的引用。所以記憶體洩漏很容易發生,當你在Activity中定義了匿名的AsyncTsk
。當非同步任務在後臺執行耗時任務期間,Activity不幸被銷燬了(譯者注:使用者退出,系統回收),這個被AsyncTask持有的Activity例項就不會被垃圾回收器回收,直到非同步任務結束。

    void startAsyncTask() {
        new AsyncTask<Void, Void, Void>() {
            @Override protected Void doInBackground(Void... params) {
                while(true);
            }
        }.execute();
    }

    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    View aicButton = findViewById(R.id.at_button);
    aicButton.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            startAsyncTask();
            nextActivity();
        }
    });

Memory Leak 4 - AsyncTask

Handler

同樣道理,定義匿名的Runnable,用匿名類Handler執行Runnable內部類會持有外部類的隱式引用,被傳遞到Handler的訊息佇列MessageQueue中,在Message訊息沒有被處理之前,Activity例項不會被銷燬了,於是導致記憶體洩漏。

    void createHandler() {
        new Handler() {
            @Override public void handleMessage(Message message) {
                super.handleMessage(message);
            }
        }.postDelayed(new Runnable() {
            @Override public void run() {
                while(true);
            }
        }, Long.MAX_VALUE >> 1);
    }


    View hButton = findViewById(R.id.h_button);
    hButton.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            createHandler();
            nextActivity();
        }
    });

Memory Leak 5 - Handler

Threads

    void spawnThread() {
        new Thread() {
            @Override public void run() {
                while(true);
            }
        }.start();
    }

    View tButton = findViewById(R.id.t_button);
    tButton.setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
          spawnThread();
          nextActivity();
      }
    });

Memory Leak 6 - Thread

TimerTask

只要是匿名類的例項,不管是不是在工作執行緒,都會持有Activity的引用,導致記憶體洩漏。

     void scheduleTimer() {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                while(true);
            }
        }, Long.MAX_VALUE >> 1);
    }

    View ttButton = findViewById(R.id.tt_button);
    ttButton.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            scheduleTimer();
            nextActivity();
        }
    });

Memory Leak 7 - TimerTask

Sensor Manager

最後,通過Context.getSystemService(int name)可以獲取系統服務。這些服務工作在各自的程序中,幫助應用處理後臺任務,處理硬體互動。如果需要使用這些服務,可以註冊監聽器,這會導致服務持有了Context的引用,如果在Activity銷燬的時候沒有登出這些監聽器,會導致記憶體洩漏。

        void registerListener() {
               SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
               Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
               sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
        }

        View smButton = findViewById(R.id.sm_button);
        smButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                registerListener();
                nextActivity();
            }
        });

Memory Leak 8 - Sensor Manager

1、靜態集合類引起記憶體洩漏:

2、當集合裡面的物件屬性被修改後,再呼叫remove()方法時不起作用。

3、監聽器

4、各種連線

5、內部類和外部模組的引用

6、單例模式

Android中常見的記憶體洩漏彙總

集合類洩漏

單例造成的記憶體洩漏

匿名內部類/非靜態內部類和非同步執行緒

這樣就在Activity內部建立了一個非靜態內部類的單例,每次啟動Activity時都會使用該單例的資料,這樣雖然避免了資源的重複建立,不過這種寫法卻會造成記憶體洩漏,因為非靜態內部類預設會持有外部類的引用,而該非靜態內部類又建立了一個靜態的例項,該例項的生命週期和應用的一樣長,這就導致了該靜態例項一直會持有該Activity的引用,導致Activity的記憶體資源不能正常回收。

Handler 造成的記憶體洩漏

Handler 的使用造成的記憶體洩漏問題應該說是最為常見了,很多時候我們為了避免 ANR 而不在主執行緒進行耗時操作,在處理網路任務或者封裝一些請求回撥等api都藉助Handler來處理,但 Handler 不是萬能的,對於 Handler 的使用程式碼編寫一不規範即有可能造成記憶體洩漏。另外,我們知道 Handler、Message 和 MessageQueue 都是相互關聯在一起的,萬一 Handler 傳送的 Message 尚未被處理,則該 Message 及傳送它的 Handler 物件將被執行緒 MessageQueue 一直持有。

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

在 Activity 中避免使用非靜態內部類,比如上面我們將 Handler 宣告為靜態的,則其存活期跟 Activity 的生命週期就無關了。同時通過弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進去。

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

前面提到了 WeakReference,所以這裡就簡單的說一下 Java 物件的幾種引用型別。

Java對引用的分類有 Strong reference, SoftReference, WeakReference, PhatomReference 四種。


Java引用的分類

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

如果只是想避免OutOfMemory異常的發生,則可以使用軟引用。如果對於應用的效能更在意,想盡快回收一些佔用記憶體比較大的物件,則可以使用弱引用。

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

建立一個靜態Handler內部類,然後對 Handler 持有的物件使用弱引用,這樣在回收時也可以回收 Handler 持有的物件,但是這樣做雖然避免了 Activity 洩漏,不過 Looper 執行緒的訊息佇列中還是可能會有待處理的訊息,所以我們在 Activity 的 Destroy 時或者 Stop 時應該移除訊息佇列 MessageQueue 中的訊息。

儘量避免使用 static 成員變數

避免 override finalize()

資源未關閉造成的記憶體洩漏

一些不良程式碼造成的記憶體壓力

有些程式碼並不造成記憶體洩露,但是它們,或是對沒使用的記憶體沒進行有效及時的釋放,或是沒有有效的利用已有的物件而是頻繁的申請新記憶體。

比如:

Bitmap 沒呼叫 recycle()方法,對於 Bitmap 物件在不使用時,我們應該先呼叫 recycle() 釋放記憶體,然後才它設定為 null. 因為載入 Bitmap 物件的記憶體空間,一部分是 java 的,一部分 C 的(因為 Bitmap 分配的底層是通過 JNI 呼叫的 )。 而這個 recyle() 就是針對 C 部分的記憶體釋放。

構造 Adapter 時,沒有使用快取的 convertView ,每次都在建立新的 converView。這裡推薦使用 ViewHolder。

總結

對 Activity 等元件的引用應該控制在 Activity 的生命週期之內; 如果不能就考慮使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部長生命週期的物件引用而洩露。

儘量不要在靜態變數或者靜態內部類中使用非靜態外部成員變數(包括context ),即使要使用,也要考慮適時把外部成員變數置空;也可以在內部類中使用弱引用來引用外部類的變數。

對於生命週期比Activity長的內部類物件,並且內部類中使用了外部類的成員變數,可以這樣做避免記憶體洩漏:

將內部類改為靜態內部類

靜態內部類中使用弱引用來引用外部類的成員變數

Handler 的持有的引用物件最好使用弱引用,資源釋放時也可以清空 Handler 裡面的訊息。比如在 Activity onStop 或者 onDestroy 的時候,取消掉該 Handler 物件的 Message和 Runnable.

在 Java 的實現過程中,也要考慮其物件釋放,最好的方法是在不使用某物件時,顯式地將此物件賦值為 null,比如使用完Bitmap 後先呼叫 recycle(),再賦為null,清空對圖片等資源有直接引用或者間接引用的陣列(使用 array.clear() ; array = null)等,最好遵循誰建立誰釋放的原則。

正確關閉資源,對於使用了BraodcastReceiver,ContentObserver,File,遊標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者登出。

保持對物件生命週期的敏感,特別注意單例、靜態物件、全域性性集合等的生命週期。

個人覺得這2篇寫的比較好,每次都要找好多資料,就給他們來了個綜合:


原文連結:http://www.jianshu.com/p/ac00e370f83d

原文連結:http://www.jianshu.com/p/66fecc0f97e6