1. 程式人生 > >Android記憶體抖動及記憶體洩漏的發現、定位和解決

Android記憶體抖動及記憶體洩漏的發現、定位和解決

記憶體抖動是指在短時間內有大量的物件被建立或者被回收的現象,記憶體抖動出現原因主要是頻繁(很重要)在迴圈裡建立物件(導致大量物件在短時間內被建立,由於新物件是要佔用記憶體空間的而且是頻繁,如果一次或者兩次在迴圈裡建立物件對記憶體影響不大,不會造成嚴重記憶體抖動這樣可以接受也不可避免,頻繁的話就很記憶體抖動很嚴重),記憶體抖動的影響是如果抖動很頻繁,會導致垃圾回收機制頻繁執行(短時間內產生大量物件,需要大量記憶體,而且還是頻繁抖動,就可能會需要回收記憶體以用於產生物件,垃圾回收機制就自然會頻繁運行了)。綜上就是頻繁記憶體抖動會導致垃圾回收頻繁執行。

記憶體洩漏是指某一段記憶體在程式裡功能上已經不需要了,但是垃圾回收機制回收記憶體時檢測那段記憶體還是被需要的,不能被回收,這種在程式中在沒有使用的但是又不能被回收的記憶體就是被洩漏的記憶體,那為什麼會這樣呢?正常的話應該是程式裡不需要的記憶體就可以被回收,這是垃圾回收機制做的事呀,如果垃圾回收機制正常執行的情況下,不應該這樣啊,但是實際就是垃圾回收機制正常的情況下發生的記憶體洩漏。其實到這裡

Java程式設計師就得知道垃圾回收機制中,判斷一段記憶體是否是垃圾,是否可回收的條件,這個條件是通過檢查這段記憶體是否存在引用和被引用關係,不存在這關係時,就認為可回收,若還存在引用或被引用關係,就認為不可回收,現在就可以知道導致記憶體洩漏的原因是程式設計師沒有將不用的記憶體去掉引用關係(因為程式中大多記憶體石油物件指向的,所以去掉引用關係就是置空)。記憶體洩漏會導致一些記憶體沒法被正常利用,話句話就是可以使用記憶體變少了,這樣輕則增加垃圾回收機制執行頻率,重則記憶體溢位(當系統需要分配一段記憶體,但是現有記憶體在垃圾回收執行後任然不足時,就會記憶體溢位);為避免記憶體洩漏,在寫程式時已經確定不需要的引用型變數,就置空;雖然即使記憶體沒洩露,也有可能出現記憶體溢位,這時的記憶體溢位就是有別的問題導致的。

1) Memory Churn and performance(記憶體抖動和效能)

雖然Android有自動管理記憶體的機制,但是對記憶體的不恰當使用仍然容易引起嚴重的效能問題。在同一幀裡面建立過多的物件是件需要特別引起注意的事情。

Android系統裡面有一個Generational Heap Memory的模型,系統會根據記憶體中不同的記憶體資料型別分別執行不同的GC操作。例如,最近剛分配的物件會放在Young Generation區域,這個區域的物件通常都是會快速被建立並且很快被銷燬回收的,同時這個區域的GC操作速度也是比Old Generation區域的GC

操作速度更快的。

 

除了速度差異之外,執行GC操作的時候,任何執行緒的任何操作都會需要暫停,等待GC操作完成之後,其他操作才能夠繼續執行(所以垃圾回收執行的次數越少,對效能的影響就越少)

 

通常來說,單個的GC並不會佔用太多時間,但是大量不停的GC操作則會顯著佔用幀間隔時間(16ms)。如果在幀間隔時間裡面做了過多的GC操作,那麼自然其他類似計算,渲染等操作的可用時間就變得少了。

導致GC頻繁執行有兩個原因:

·Memory Churn記憶體抖動,記憶體抖動是因為大量的物件被建立又在短時間內馬上被釋放。

·瞬間產生大量的物件會嚴重佔用Young Generation的記憶體區域,當達到閥值,剩餘空間不夠的時候,也會觸發GC。即使每次分配的物件佔用了很少的記憶體,但是他們疊加在一起會增加Heap的壓力,從而觸發更多其他型別的GC。這個操作有可能會影響到幀率,並使得使用者感知到效能問題(幀率是Android渲染機制中的概念,導致卡頓慢的直接原因,就是渲染機制受阻,關於渲染機制有另一篇部落格特別說了,想了解的可以點選這裡)。

 

解決上面的問題有簡潔直觀方法,如果你在Memory Monitor裡面檢視到短時間發生了多次記憶體的漲跌,這意味著很有可能發生了記憶體抖動。

 

同時我們還可以通過Allocation Tracker來檢視在短時間內,同一個棧中不斷進出的相同物件。這是記憶體抖動的典型訊號之一。

當你大致定位問題之後,接下去的問題修復也就顯得相對直接簡單了。例如,你需要避免在for迴圈裡面分配物件佔用記憶體,需要嘗試把物件的建立移到迴圈體之外,自定義View中的onDraw方法也需要引起注意,每次螢幕發生繪製以及動畫執行過程中,onDraw方法都會被呼叫到,避免在onDraw方法裡面執行復雜的操作,避免建立物件。對於那些無法避免需要建立物件的情況,我們可以考慮物件池模型,通過物件池來解決頻繁建立與銷燬的問題,但是這裡需要注意結束使用之後,需要手動釋放物件池中的物件。

2) Garbage Collection in Android(Android垃圾回收)

JVM的回收機制給開發人員帶來很大的好處,不用時刻處理物件的分配與回收,可以更加專注於更加高階的程式碼實現。相比起JavaCC++等語言具備更高的執行效率,他們需要開發人員自己關注物件的分配與回收,但是在一個龐大的系統當中,還是免不了經常發生部分物件忘記回收的情況,這就是記憶體洩漏。

原始JVM中的GC機制在Android中得到了很大程度上的優化。Android裡面是一個三級Generation的記憶體模型,最近分配的物件會存放在Young Generation區域,當這個物件在這個區域停留的時間達到一定程度,它會被移動到Old Generation,最後到Permanent Generation區域。

 

每一個級別的記憶體區域都有固定的大小,此後不斷有新的物件被分配到此區域,當這些物件總的大小快達到這一級別記憶體區域的閥值時,會觸發GC的操作,以便騰出空間來存放其他新的物件。

 

前面提到過每次GC發生的時候,所有的執行緒都是暫停狀態的。GC所佔用的時間和它是哪一個Generation也有關係,Young Generation的每次GC操作時間是最短的,Old Generation其次,Permanent Generation最長。執行時間的長短也和當前Generation中的物件數量有關,遍歷查詢20000個物件比起遍歷50個物件自然是要慢很多的。

雖然Google的工程師在儘量縮短每次GC所花費的時間,但是特別注意GC引起的效能問題還是很有必要。如果不小心在最小的for迴圈單元裡面執行了建立物件的操作,這將很容易引起GC並導致效能問題。通過Memory Monitor我們可以檢視到記憶體的佔用情況,每一次瞬間的記憶體降低都是因為此時發生了GC操作,如果在短時間內發生大量的記憶體上漲與降低的事件(記憶體嚴重抖動),這說明很有可能這裡有效能問題。我們還可以通過Heap and Allocation Tracker工具來檢視此時記憶體中分配的到底有哪些物件。

到這裡為止就簡單介紹了記憶體抖動(概念及判斷方法,方法是兩個工具使用,一個用於判斷有沒有嚴重記憶體抖動Memory Monitor,一個用於確認抖動位置Heap and Allocation Tracker),及較詳細的介紹了Android中垃圾回收機制。

2) Performance Cost of Memory Leaks(記憶體洩漏)

雖然Java有自動回收的機制,可是這不意味著Java中不存在記憶體洩漏的問題,而記憶體洩漏會很容易導致嚴重的效能問題。

記憶體洩漏指的是那些程式不再使用的物件無法被GC識別,這樣就導致這個物件一直留在記憶體當中,佔用了寶貴的記憶體空間。顯然,這還使得每級Generation的記憶體區域可用空間變小,GC就會更容易被觸發,從而引起效能問題。

尋找記憶體洩漏並修復這個漏洞是件很棘手的事情,你需要對執行的程式碼很熟悉,清楚的知道在特定環境下是如何執行的,然後仔細排查。例如,你想知道程式中的某個activity退出的時候,它之前所佔用的記憶體是否有完整的釋放乾淨了?首先你需要在activity處於前臺的時候使用Heap Tool獲取一份當前狀態的記憶體快照,然後你需要建立一個幾乎不這麼佔用記憶體的空白activity用來給前一個Activity進行跳轉,其次在跳轉到這個空白的activity的時候主動呼叫System.gc()方法來確保觸發一個GC操作。最後,如果前面這個activity的記憶體都有全部正確釋放,那麼在空白activity被啟動之後的記憶體快照中應該不會有前面那個activity中的任何物件了。


關於記憶體抖動和記憶體洩漏就到這裡了,接下來就說一下Android studio 提供的記憶體優化方面的工具

Android Studio提供了工具來幫助開發者發現和解決記憶體抖動和記憶體洩漏。

 Tool - Memory Monitor(用於發現記憶體抖動及記憶體洩漏的)

Android Studio中的Memory Monitor可以很好的幫組我們檢視程式的記憶體使用情況。


以下內容很重要,以下內容很重要,以下內容很重要重要的事情說三遍

·Memory Monitor:檢視整個app所佔用的記憶體,以及發生GC的時刻,短時間內發生大量的GC操作是一個危險的訊號(用於發現有沒有記憶體洩漏和嚴重記憶體抖動)。

後面兩個是用於定位的記憶體抖動和記憶體洩漏發生的具體位置·

Allocation Tracker:使用此工具來追蹤記憶體的分配,前面有提到過。

Heap Tool:檢視當前記憶體快照,便於對比分析哪些物件有可能是洩漏了的.


現在就可以定位到某一段程式碼發生了記憶體洩漏或抖動。

如果是記憶體洩漏解決方法就很直接,在適當的時候把洩漏的物件置空就可以了。

但是如果只記憶體抖動的話就得分兩種情況,由於記憶體抖動是在短時間內建立釋放大量物件導致的(一般是迴圈內建立物件),直接辦法就是不再短時間內建立大量物件,如果建立物件的過程可以拿到迴圈外而不影響功能,這種情況比較容易解決。但是更多的是另一種情況,就是不能拿到迴圈外,否則影響功能。對於第二種情況就要做到在迴圈內建立物件,但是又要控制物件個數,這個問題目前可以使用物件池的方法解決。

 3)Object Pools

在程式裡面經常會遇到的一個問題是短時間內建立大量的物件,導致記憶體緊張,從而觸發GC導致效能問題。對於這個問題,我們可以使用物件池技術來解決它。通常物件池中的物件可能是bitmapsviewspaints等等。關於物件池的操作原理,不展開述說了,請看下面的圖示:

 

使用物件池技術有很多好處,它可以避免記憶體抖動,提升效能,但是在使用的時候有一些內容是需要特別注意的。通常情況下,初始化的物件池裡面都是空白的,當使用某個物件的時候先去物件池查詢是否存在,如果不存在則建立這個物件然後加入物件池,但是我們也可以在程式剛啟動的時候就事先為物件池填充一些即將要使用到的資料,這樣可以在需要使用到這些物件的時候提供更快的首次載入速度,這種行為就叫做預分配。使用物件池也有不好的一面,程式設計師需要手動管理這些物件的分配與釋放,所以我們需要慎重地使用這項技術,避免發生物件的記憶體洩漏。為了確保所有的物件能夠正確被釋放,我們需要保證加入物件池的物件和其他外部物件沒有互相引用的關係。

其實物件池給筆者感覺與執行緒池相似,不同的是重心不同,執行緒池考慮的是執行速度提高(使用預先產生空閒執行緒的方式),物件池更側重與數量(可能是分配物件記憶體時間是很短的,所以不需要預分配,導致物件池的預分配優勢不明顯)。

現在問題還沒解決呢,關於解決記憶體抖動,物件池很好,但是僅僅是一個思想概念,沒具體化。怎麼實現呢,筆者推薦使用Java的一個LinkedHashMap 這個類,與普通hashmap有不同,就是可以控制數量關於LinkedHashMap 更詳細的資訊,筆者已轉載一篇感覺很棒的關於LinkedHashMap的部落格,點選這裡可檢視

在這裡Android已提供了一個類可以解決控制數量問題

LRU Cache 通過使用LinkedHashMap實現了LRU Cache (最近最少使用)演算法,這是作業系統的一個演算法,具體的自己百度很多,在這裡不祥細說明。

LRUCache 的實現和使用,筆者也轉載了一篇部落格,感覺很全點選這裡可檢視,看了媽媽再也不用擔心我的Android程式出現記憶體抖動了。


一般情況下,常見發生記憶體洩漏定位到的地方及解決方法如下:

1.集合類

集合類如果僅僅有新增元素的機制,而沒有相應刪除元素機制,這樣就會造成記憶體被佔用,如果這個類是全域性性變數(比如類中有靜態屬性,全域性性的map等即有靜態引用或final一直指向它)。那麼沒有相應刪除機制,很可能導致集合所佔記憶體只增不減。  解決辦法:在使用集合類時,增加刪除元素機制,並適當呼叫減少集合所佔記憶體。

2.單例模式

不正確使用單例模式,也會引起記憶體洩漏單例物件在初始化後將在JVM的整個生命週期存在(以靜態變數方式),如果單例物件持有外部物件的引用,那麼這個外部物件就會一直佔用著記憶體,可能導致記憶體洩漏(取決於這外部物件是否一致有用)。   解決辦法:單例物件中避免含有不是一直都有用的外部物件引用。

3.Android元件或特殊集合物件的使用

BraodcastReceiver ,ContentObserver,fileObserver,Cursor,Callback等在Activity onDestory或者某類生命週期結束之後一定要unregistere或者close掉,否則這個Activity類會被system強引用,不會被回收。不要直接對Activity進行直接引用作為成員變數,如果不得不這麼做,呼叫private WeakPeferense mActivity 來做,相同的,對與Service等其他有自己生命週期的物件來說,直接引用都需要考慮是否會存在記憶體洩露的可能。

4.Handler

要知道,只要Handler 傳送的Message尚未被處理,則該Message及傳送它的Handler物件將被執行緒MessageQueue一直持有。由於Handler屬於TLSThread Local Storage)變數,生命週期和Activity是不一致的。因此這種實現方式一般很難保證跟view或者Activity的生命週期保持一致,故很容易導致無法正確釋放。如上所述,Handler使用要特別小心,否則很可能記憶體洩漏。   解決辦法:在view 或者Activity生命週期結束前,確保Handler已沒有未處理的訊息(特別是延時訊息)。

5.Thread 記憶體洩漏

執行緒也是造成記憶體洩露的一個重要源頭,執行緒產生記憶體洩露的主要原因在於執行緒生命週期不可控,比如執行緒是Activity的內部類,則執行緒物件中儲存了Activity的一個引用,當執行緒的run函式耗時較長沒有結束時,執行緒物件是不會被銷燬的,因此它所引用的老的Activity就出現了記憶體洩漏問題。解決辦法:1.簡化執行緒run函式執行的任務,使他在Activity生命週期結束前,任務執行完。2.Thread增加撤銷機制,當Activity生命週期結束時,將Thread的耗時任務撤銷(筆者推薦這種)。

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

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

(1) Bitmap 沒呼叫recycle()

Bitmap 物件在不使用時,我們應該先呼叫recycle()釋放記憶體,然後才置空,因為載入bitmap物件的記憶體空間,一部分是java的,一部分是c的(因為Bitmap分配的底層是通過jni呼叫的,Android的Bitmap底層是使用skia圖形庫實現,skia是用c實現的)。這個recycle()函式就是針對c部分的記憶體釋放。

2)構造Adapter時,沒有使用快取的convertView。   解決辦法:使用靜態holdview的方式構造Adapter


轉載自:

http://blog.csdn.net/huang_rong12/article/details/51628264

http://blog.csdn.net/huang_rong12/article/details/51628750