Android記憶體洩露小結
Java 程式執行時的記憶體分配策略有三種,分別是靜態分配,棧式分配,和堆式分配,對應的,三種儲存策略使用的記憶體空間主要分別是靜態儲存區(也稱方法區)、棧區和堆區。
- 靜態儲存區(方法區):主要存放靜態資料、全域性 static 資料和常量。這塊記憶體在程式編譯時就已經分配好,並且在程式整個執行期間都存在。
- 棧區 :當方法被執行時,方法體內的區域性變數(其中包括基礎資料型別、物件的引用)都在棧上建立,並在方法執行結束時這些區域性變數所持有的記憶體將會自動被釋放。因為棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限。
- 堆區 : 又稱動態記憶體分配,通常就是指在程式執行時直接 new 出來的記憶體,也就是物件的例項。這部分記憶體在不使用時將會由 Java 垃圾回收器來負責回收。
棧與堆的區別:
棧區:因為棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限。所以棧區一般只給方法體內的變數分配記憶體空間,
堆區:空間容量足夠,會給物件,及類中的變數開闢空間。
在方法體內定義的(區域性變數)一些基本型別的變數和物件的引用變數都是在方法的棧記憶體中分配的。當在一段方法塊中定義一個變數時,Java 就會在棧中為該變數分配記憶體空間,當超過該變數的作用域後,該變數也就無效了,分配給它的記憶體空間也將被釋放掉,該記憶體空間可以被重新使用。
堆記憶體用來存放所有由 new 建立的物件(包括該物件其中的所有成員變數)和陣列。在堆中分配的記憶體,將由 Java 垃圾回收器來自動管理。在堆中產生了一個數組或者物件後,還可以在棧中定義一個特殊的變數,這個變數的取值等於陣列或者物件在堆記憶體中的首地址,這個特殊的變數就是我們上面說的引用變數。我們可以通過這個引用變數來訪問堆中的物件或者陣列。
例項:
public class Sample { int s1 = 0; Sample mSample1 = new Sample(); public void method() { int s2 = 1; Sample mSample2 = new Sample(); } } Sample mSample3 = new Sample(); 複製程式碼
Sample 類的區域性變數 s2 和引用變數 mSample2 都是存在於棧中,但 mSample2 指向的物件是存在於堆上的。 mSample3 指向的物件實體存放在堆上,包括這個物件的所有成員變數 s1 和 mSample1,而它自己存在於棧中。
結論:
區域性變數的基本資料型別和引用儲存於棧中,引用的物件實體儲存於堆中。—— 因為它們屬於方法中的變數,生命週期隨方法而結束。
成員變數全部儲存與堆中(包括基本資料型別,引用和引用的物件實體)—— 因為它們屬於類,類物件終究是要被new出來使用的。
Java是如何管理記憶體
Java的記憶體管理就是物件的分配和釋放問題。在 Java 中,程式設計師需要通過關鍵字 new 為每個物件申請記憶體空間 (基本型別除外),所有的物件都在堆 (Heap)中分配空間。另外,物件的釋放是由 GC 決定和執行的。在 Java 中,記憶體的分配是由程式完成的,而記憶體的釋放是由 GC 完成的,這種收支兩條線的方法確實簡化了程式設計師的工作。但同時,它也加重了JVM的工作。這也是 Java 程式執行速度較慢的原因之一。因為,GC 為了能夠正確釋放物件,GC 必須監控每一個物件的執行狀態,包括物件的申請、引用、被引用、賦值等,GC 都需要進行監控。
監視物件狀態是為了更加準確地、及時地釋放物件,而釋放物件的根本原則就是該物件不再被引用。
為了更好理解 GC 的工作原理,我們可以將物件考慮為有向圖的頂點,將引用關係考慮為圖的有向邊,有向邊從引用者指向被引物件。另外,每個執行緒物件可以作為一個圖的起始頂點,例如大多程式從 main 程序開始執行,那麼該圖就是以 main 程序頂點開始的一棵根樹。在這個有向圖中,根頂點可達的物件都是有效物件,GC將不回收這些物件。如果某個物件 (連通子圖)與這個根頂點不可達(注意,該圖為有向圖),那麼我們認為這個(這些)物件不再被引用,可以被 GC 回收。 以下,我們舉一個例子說明如何用有向圖表示記憶體管理。對於程式的每一個時刻,我們都有一個有向圖表示JVM的記憶體分配情況。以下右圖,就是左邊程式執行到第6行的示意圖。

Java使用有向圖的方式進行記憶體管理,可以消除引用迴圈的問題,例如有三個物件,相互引用,只要它們和根程序不可達的,那麼GC也是可以回收它們的。這種方式的優點是管理記憶體的精度很高,但是效率較低。另外一種常用的記憶體管理技術是使用計數器,例如COM模型採用計數器方式管理構件,它與有向圖相比,精度行低(很難處理迴圈引用的問題),但執行效率很高。
什麼是Java中的記憶體洩露
在Java中,記憶體洩漏就是存在一些被分配的物件,這些物件有下面兩個特點,首先,這些物件是可達的,即在有向圖中,存在通路可以與其相連;其次,這些物件是無用的,即程式以後不會再使用這些物件。如果物件滿足這兩個條件,這些物件就可以判定為Java中的記憶體洩漏,這些物件不會被GC所回收,然而它卻佔用記憶體。
Java記憶體洩漏引起的原因
- 靜態集合類引起記憶體洩漏:
像HashMap、Vector等的使用最容易出現記憶體洩露,這些靜態變數的生命週期和應用程式一致,他們所引用的所有的物件Object也不能被釋放,因為他們也將一直被Vector等引用著。
Static Vector v = new Vector(10); for (int i = 1; i<100; i++) { Object o = new Object(); v.add(o); o = null; } 複製程式碼
在這個例子中,迴圈申請Object 物件,並將所申請的物件放入一個Vector 中,如果僅僅釋放引用本身(o=null),那麼Vector 仍然引用該物件,所以這個物件對GC 來說是不可回收的。因此,如果物件加入到Vector 後,還必須從Vector 中刪除,最簡單的方法就是將Vector物件設定為null。
-
當集合裡面的物件屬性被修改後,再呼叫remove()方法時不起作用。
public static void main(String[] args){ Set set = new HashSet(); Person p1 = new Person("唐僧","pwd1",25); Person p2 = new Person("孫悟空","pwd2",26); Person p3 = new Person("豬八戒","pwd3",27); set.add(p1); set.add(p2); set.add(p3); System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:3 個元素! p3.setAge(2); //修改p3的年齡,此時p3元素對應的hashcode值發生改變
set.remove(p3); //此時remove不掉,造成記憶體洩漏 set.add(p3); //重新新增,居然新增成功 System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:4 個元素! for (Person person : set) { System.out.println(person); } 複製程式碼
}
-
監聽器 在java 程式設計中,我們都需要和監聽器打交道,通常一個應用當中會用到很多監聽器,我們會呼叫一個控制元件的諸如addXXXListener()等方法來增加監聽器,但往往在釋放物件的時候卻沒有記住去刪除這些監聽器,從而增加了記憶體洩漏的機會。
因為物件還和監聽器保持聯絡,所有GC會認為這個物件還有用,不會去回收。
- 各種連線
比如資料庫連線(dataSourse.getConnection()),網路連線(socket)和io連線,除非其顯式的呼叫了其close()方法將其連線關閉,否則是不會自動被GC 回收的。對於Resultset 和Statement 物件可以不進行顯式回收,但Connection 一定要顯式回收,因為Connection 在任何時候都無法自動回收,而Connection一旦回收,Resultset 和Statement 物件就會立即為NULL。但是如果使用連線池,情況就不一樣了,除了要顯式地關閉連線,還必須顯式地關閉Resultset Statement 物件(關閉其中一個,另外一個也會關閉),否則就會造成大量的Statement 物件無法釋放,從而引起記憶體洩漏。這種情況下一般都會在try裡面去的連線,在finally裡面釋放連線。
Android下常見的記憶體洩露
1.集合類洩漏
集合類如果僅僅有新增元素的方法,而沒有相應的刪除機制,導致記憶體被佔用。如果這個集合類是全域性性的變數 (比如類中的靜態屬性,全域性性的 map 等即有靜態引用或 final 一直指向它),那麼沒有相應的刪除機制,很可能導致集合所佔用的記憶體只增不減。比如上面的典型例子就是其中一種情況,當然實際上我們在專案中肯定不會寫這麼 2B 的程式碼,但稍不注意還是很容易出現這種情況,比如我們都喜歡通過 HashMap 做一些快取之類的事,這種情況就要多留一些心眼。
2.單例造成的記憶體洩漏
//這個時候傳過來的context如果是activity的context就會造成記憶體洩露,因為單例是靜態的,會導致對於的activity無法釋放, //解決方法,傳過來一個Appliaction的context,或者Application寫一個靜態方法 返回一個context 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) { instance = new AppManager(context); } return instance; } } 複製程式碼
3.非靜態內部類建立靜態例項造成的記憶體洩漏
4.匿名內部類
android開發經常會繼承實現Activity/Fragment/View,此時如果你使用了匿名類,並被非同步執行緒持有了,那要小心了,如果沒有任何措施這樣一定會導致洩露
public class MainActivity extends Activity { ... Runnable ref1 = new MyRunable(); Runnable ref2 = new Runnable() { @Override public void run() { } }; ... } 複製程式碼
ref1和ref2的區別是,ref2使用了匿名內部類。我們來看看執行時這兩個引用的記憶體:

可以看到,ref1沒什麼特別的。
但ref2這個匿名類的實現物件裡面多了一個引用:
this$0這個引用指向MainActivity.this,也就是說當前的MainActivity例項會被ref2持有,如果將這個引用再傳入一個非同步執行緒,此執行緒和此Acitivity生命週期不一致的時候,就造成了Activity的洩露。
5.Handler 造成的記憶體洩漏
handler主要是為了處理耗時操作存在的,而且Handler,Message和MessageQueue是配套存在的。
當Handler傳送的message沒有被及時處理時(比如sleep),執行緒MessageQueue會一直持有Message和傳送它的Handler物件,同時也會引起對於的Activity無法及時釋放,從而引起記憶體洩露。
解決方法:將Handler宣告為靜態內部類,這樣Activity的生命週期就不會受到Activity的影響了。
推薦使用靜態內部類 + WeakReference 這種方式。每次使用前注意判空。

在Android應用的開發中,為了防止記憶體溢位,在處理一些佔用記憶體大而且宣告週期較長的物件時候,可以儘量應用軟引用和弱引用技術。
軟/弱引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收器回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。利用這個佇列可以得知被回收的軟/弱引用的物件列表,從而為緩衝器清除已失效的軟/弱引用。
假設我們的應用會用到大量的預設圖片,比如應用中有預設的頭像,預設遊戲圖示等等,這些圖片很多地方會用到。如果每次都去讀取圖片,由於讀取檔案需要硬體操作,速度較慢,會導致效能較低。所以我們考慮將圖片快取起來,需要的時候直接從記憶體中讀取。但是,由於圖片佔用記憶體空間比較大,快取很多圖片需要很多的記憶體,就可能比較容易發生OutOfMemory異常。這時,我們可以考慮使用軟/弱引用技術來避免這個問題發生。
使用軟引用以後,在OutOfMemory異常發生之前,這些快取的圖片資源的記憶體空間可以被釋放掉的,從而避免記憶體達到上限,避免Crash發生。
如果只是想避免OutOfMemory異常的發生,則可以使用軟引用。如果對於應用的效能更在意,想盡快回收一些佔用記憶體比較大的物件,則可以使用弱引用。
另外可以根據物件是否經常使用來判斷選擇軟引用還是弱引用。如果該物件可能會經常使用的,就儘量用軟引用。如果該物件不被使用的可能性更大些,就可以用弱引用。
ok,繼續回到主題。前面所說的,建立一個靜態Handler內部類,然後對 Handler 持有的物件使用弱引用,這樣在回收時也可以回收 Handler 持有的物件,但是這樣做雖然避免了 Activity 洩漏,不過 Looper 執行緒的訊息佇列中還是可能會有待處理的訊息,所以我們在 Activity 的 Destroy 時或者 Stop 時應該移除訊息佇列 MessageQueue 中的訊息。
下面幾個方法都可以移除 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); 複製程式碼
###6.儘量避免使用 static 成員變數 ### 如果成員變數被宣告為 static,那我們都知道其生命週期將與整個app程序生命週期一樣。
這會導致一系列問題,如果你的app程序設計上是長駐記憶體的,那即使app切到後臺,這部分記憶體也不會被釋放。按照現在手機app記憶體管理機制,佔記憶體較大的後臺程序將優先回收,因為如果此app做過程序互保保活,那會造成app在後臺頻繁重啟。當手機安裝了你參與開發的app以後一夜時間手機被消耗空了電量、流量,你的app不得不被使用者解除安裝或者靜默。
這裡修復的方法是:
不要在類初始時初始化靜態成員。可以考慮lazy初始化。 架構設計上要思考是否真的有必要這樣做,儘量避免。如果架構需要這麼設計,那麼此物件的生命週期你有責任管理起來。
7.避免 override finalize() --手動GC
1、finalize 方法被執行的時間不確定,不能依賴與它來釋放緊缺的資源。時間不確定的原因是: 虛擬機器呼叫GC的時間不確定 Finalize daemon執行緒被排程到的時間不確定
2、finalize 方法只會被執行一次,即使物件被複活,如果已經執行過了 finalize 方法,再次被 GC 時也不會再執行了,原因是:
含有 finalize 方法的 object 是在 new 的時候由虛擬機器生成了一個 finalize reference 在來引用到該Object的,而在 finalize 方法執行的時候,該 object 所對應的 finalize Reference 會被釋放掉,即使在這個時候把該 object 復活(即用強引用引用住該 object ),再第二次被 GC 的時候由於沒有了 finalize reference 與之對應,所以 finalize 方法不會再執行。
3、含有Finalize方法的object需要至少經過兩輪GC才有可能被釋放。
8.資源未關閉造成的記憶體洩漏
對於使用了BraodcastReceiver,ContentObserver,File,遊標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者登出,否則這些資源將不會被回收,造成記憶體洩漏。
9.一些不良程式碼造成的記憶體壓力
有些程式碼並不造成記憶體洩露,但是它們,或是對沒使用的記憶體沒進行有效及時的釋放,或是沒有有效的利用已有的物件而是頻繁的申請新記憶體。
比如: Bitmap 沒呼叫 recycle()方法,對於 Bitmap 物件在不使用時,我們應該先呼叫 recycle() 釋放記憶體,然後才它設定為 null. 因為載入 Bitmap 物件的記憶體空間,一部分是 java 的,一部分 C 的(因為 Bitmap 分配的底層是通過 JNI 呼叫的 )。 而這個 recyle() 就是針對 C 部分的記憶體釋放。 構造 Adapter 時,沒有使用快取的 convertView ,每次都在建立新的 converView。這裡推薦使用 ViewHolder。