面試中經常會問到內存優化,我們在開發過程中也多少會遇到OOM的問題,根據大牛們的博客,記錄下我的學習思路
一、爲何會OOM?
1. 一直以來Andorid手機的內存都比iPhone(iPhone6RAM1G)大的多,Android卻經常出現OOM,這是爲何?
這個是因爲Android系統對dalvik的vm heapsize 作了硬性限制,當java進程申請的java空間超過閥值時,就會拋出OOM異常(這個閥值可以是48M、24M、16M等,視機型而定),可以通過adb shell getprop | grep dalvik.vm.heapgrowthlimit查看此值。(我的一加2 Android6.0.1已經達到了256M)
也就是說,程序發生OOM並不表示RAM不足,而是因爲程序申請的java heap對象超過了dalvik.vm.heapgrowthlimit。也就是說,在RAM充足的情況下,也可能發生OOM
這樣的設計似乎有些不合理,但是Google爲什麼這樣做呢?這樣設計的目的是爲了讓Android系統能同時讓比較多的進程常駐內存,這樣程序啓動時就不用每次都重新加載到內存,能夠給用戶更快的響應。迫使每個應用程序使用較小的內存,移動設備非常有限的RAM就能使比較多的app常駐其中。
2. 大型遊戲如何在較小的heapsize上運行?
創建子進程
創建一個新的進程,那麼我們就可以把一些對象分配到新進程的heap上了,從而達到一個應用程序使用更多的內存的目的,當然,創建子進程會增加系統開銷,而且並不是所有應用程序都適合這樣做,視需求而定。
創建子進程的方法:使用android:process標籤
使用jni在native heap上申請空間(推薦使用)
nativeheap的增長並不受dalvik vm heapsize的限制,從圖6可以看出這一點,它的native heap size已經遠遠超過了dalvik heap size的限制。
只要RAM有剩餘空間,程序員可以一直在native heap上申請空間,當然如果 RAM快耗盡,memory killer會殺進程釋放RAM。大家使用一些軟件時,有時候會閃退,就可能是軟件在native層申請了比較多的內存導致的。比如,我(餘龍飛)就碰到過UC web在瀏覽內容比較多的網頁時閃退,原因就是其native heap增長到比較大的值,佔用了大量的RAM,被memory killer殺掉了。(Fresco使用的就是這種方式)
使用顯存(操作系統預留RAM的一部分作爲顯存)
使用OpenGL textures等API,texture memory不受dalvik vm heapsize限制,這個我沒有實踐過。再比如Android中GraphicBufferAllocator申請的內存就是顯存。
3. Android內存究竟如何?(native heap、java heap)
進程的地址空間
在32位操作系統中(Native Process),進程的地址空間爲0到4GB:
- kernel space(內河空間):這些地址用戶代碼不能讀也不能寫
- Memory Mapping Segment(內存映射):段文件映射(包括動態庫)和匿名映射。
- Stack:(進棧和出棧)由操作系統控制,其中主要存儲函數地址、函數參數、局部變量等等,所以Stack空間不需要很大,一般爲幾MB大小。
- Heap:空間的使用由程序員控制,程序員可以使用malloc、new、free、delete等函數調用來操作這片地址空間。Heap爲程序完成各種複雜任務提供內存空間,所以空間比較大,一般爲幾百MB到幾GB。正是因爲Heap空間由程序員管理,所以容易出現使用不當導致嚴重問題。
Android中的進程
native進程:採用C/C++實現,不包含dalvik實例的linux進程,/system/bin/目錄下面的程序文件運行後都是以native進程形式存在的。如下圖/system/bin/surfaceflinger、/system/bin/rild、procrank等就是native進程。
java進程:實例化了dalvik虛擬機實例的linux進程,進程的入口main函數爲java函數。dalvik虛擬機實例的宿主進程是fork()系統調用創建的linux進程,所以每一個android上的java進程實際上就是一個linux進程,只是進程中多了一個dalvik虛擬機實例。因此,java進程的內存分配比native進程複雜。如圖3,Android系統中的應用程序基本都是java進程,如桌面、電話、聯繫人、狀態欄等等。
Android中進程的堆內存
第一張圖和下面這張圖分別介紹了native process和java process的結構,這個是我們需要深刻理解的,進程空間中的heap空間是我們需要重點關注的。heap空間完全由程序員控制,我們使用的malloc、C++ new和java new所申請的空間都是heap空間, C/C++申請的內存空間在native heap中,而java申請的內存空間則在dalvik heap中。
注:Java中的code segment,data segment,heap,stack stack(棧):對象引用都是在棧裏的,相當於C/C++的指針 heap(堆):new出來的對象實例纔是在堆裏 data segment:一般存放常量和靜態常量 code segment:方法,函數什麼的都是放在code segment
Bitmap分配在native heap還是dalvik heap上?
3.0後是分配在dalvik heap上,和3.x之前是分配在native heap
4. 以上主要來自:現任支付寶大神餘龍飛著作——Android進程的內存管理分析
二、內存分析之MAT
1. 谷歌提供了幾種內存檢測工具:
- Memory Monitor:內存監視器
- Heap Viewer:堆查看器
- Allocation Tracker:分配追蹤器
Investigating Your RAM Usage:調查您的RAM使用
通過Log輸出的GC命令來判斷: GC_CONCURRENT:heap快滿了 GC_FOR_MALLOC:因爲你的應用程序試圖分配內存時,你已經充分引起GC堆,所以系統必須停止你的應用和回收內存。 GC_HPROF_DUMP_HEAP:GC發生時,你要創建的請求HPROF文件來分析你的堆。 GC_EXPLICIT:一個明確的GC,當收到調用gc()時出現,應該儘量避免手動調用,而是相信GC會自動清理 GC_EXTERNAL_ALLOC:只會在API10以及以下才會出現 GC的原因: Concurrent:不會暫停應用線程,在後臺運行,不會影響內存分配 Alloc:GC是因爲你的應用程序試圖分配內存時,你heapwas已滿。在這種情況下,垃圾收集發生在分配線程。 Explicit:手動調用gc(),我們應該避免手動調用,我們要相信GC,手動調用會影響線程分配以及沒必要的cpu週期,還可能導致其他線程的搶佔。 NativeAlloc:native內存的回收,主要來自人爲造成的native內存壓力,例如:Bitmap、渲染腳本分配的對象 CollectorTransition:.....由於用到的太少,後面的就不再詳述
2. 觸發內存泄漏
- 多次切換屏幕的橫縱,在Activity不同狀態旋轉屏幕,然後再返回。旋轉設備往往會導致應用程序的Activity、Context、或View對象泄漏,因爲系統中重新創建活動,如果程序中其他地方擁有這些對象的引用,系統無法回收。
- 多個應用之間切換,在Activity不同的狀態下切換(導航至主屏幕,然後返回到您的應用程序)。
3. 怎樣的內存是健康的?
內存使用率低,使用率穩定(波動小)
- 沒有正在使用的對象,要
能夠被GC回收
(避免內存泄漏) 不再使用的內存對象、或着大型內存,
使用結束(虛引用)馬上回收
(finalize()方法進行清理
,通過 java.lang.ref.PhantomReference實現)我們進行內存分析具體分析什麼呢? 1. 大型對象 2. 不使用的未能被釋放的對象(內存泄漏) 而谷歌目前提供的內存分析工具只能從宏觀上進行內存分析,無法針對某個對象進行分析 這裏我們這裏需要使用強大的第三方內存分析工具MAT(Memory Analyzer Tool)針對具體內存進行分析
4. MAT基礎知識
MAT簡介
- MAT(Memory Analyzer Tool),一個基於Eclipse的內存分析工具,是一個快速、功能豐富的JAVA heap分析工具,它可以幫助我們查找內存泄漏和減少內存消耗。使用內存分析工具從衆多的對象中進行分析,快速的計算出在內存中對象的佔用大小,看看是誰阻止了垃圾收集器的回收工作,並可以通過報表直觀的查看到可能造成這種結果的對象。
- 當然MAT也有獨立的不依賴Eclipse的版本,只不過這個版本在調試Android內存的時候,需要將DDMS生成的文件進行轉換,纔可以在獨立版本的MAT上打開。不過Android SDK中已經提供了這個Tools,所以使用起來也是很方便的。
MAT下載地址
獨立版本下載地址: https://eclipse.org/mat/downloads.php
這種方式有個麻煩的地方就是DDMS導出的文件,需要進行轉換纔可以在MAT中打開。Eclipse插件地址:http://download.eclipse.org/mat/1.5/update-site/
MAT中重要概念介紹
要看懂MAT的列表信息,Shallow heap、Retained Heap、GC Root 這幾個概念一定要弄懂。
Shallow heap
Shallow size就是對象本身佔用內存的大小,不包含其引用的對象。
常規對象
(非數組)的Shallow size有其成員變量的數量和類型決定。
數組的shallow size有數組元素的類型(對象類型、基本類型)和數組長度決定
因爲不像c++的對象本身可以存放大量內存,java的對象成員都是些引用。真正的內存都在堆上,看起來是一堆原生的byte[],char[], int[],所以我們如果只看對象本身的內存,那麼數量都很小。所以我們看到Histogram圖是以Shallow size進行排序的,排在第一位第二位的是byte,char 。
Retained Heap
Retained Heap的概念:如果一個對象被釋放掉,那麼該對象引用的所有對象(包括被遞歸釋放的)佔用的heap也會被釋放。
如果一個對象的某個成員new了一大塊int數組,那這個int數組也可以計算到這個對象中。
與shallow heap比較,Retained heap可以更精確的反映一個對象實際佔用的大小
(因爲如果該對象釋放,retained heap都可以被釋放)。注意:A和B都引用到同一內存,A釋放時,該內存不會被釋放。所以這塊內存不會被計算到A或者B的Retained Heap中。故
Retained Heap並不總是那麼有效。
這一點並不重要,因爲MAT引入了Dominator Tree--對象引用樹,計算Retained Heap就會非常方便,顯示也非常方便。對應到MAT UI上,在dominator tree這個view中,顯示了每個對象的shallow heap和retained heap。然後可以以該節點爲樹根,一步步的細化看看retained heap到底是用在什麼地方了。
GC Root
GC發現通過任何reference chain(引用鏈)無法訪問某個對象的時候,該對象即被回收。
名詞GC Roots正是分析這一過程的起點,例如JVM自己確保了對象的可到達性(那麼JVM就是GC Roots),所以GC Roots就是這樣在內存中保持對象可到達性的,一旦不可到達,即被回收。
通常GC Roots是一個在current thread(當前線程)的call stack(調用棧)上的對象(例如方法參數和局部變量),或者是線程自身或者是system class loader(系統類加載器)加載的類以及native code(本地代碼)保留的活動對象。所以
GC Roots是分析對象爲何還存活於內存中的利器。
MAT界面功能介紹
- 打開經過轉換的hprof文件:
可不選 Actions區域,幾種分析方法:
Histogram:列出內存中的對象,對象的個數以及大小
Dominator Tree:列出最大的對象以及其依賴存活的Object (大小是以Retained Heap爲標準排序的)
點開每個對象,
檢查內部的超大對象
- Top Consumers : 通過圖形列出最大的object
一般Histogram和 Dominator Tree是最常用的。
- 打開經過轉換的hprof文件:
MAT分析對象的引用
Path to GC Root
在Histogram或者Domiantor Tree的某一個條目上,右鍵可以查看其GC Root Path:
點擊Path To GC Roots –> with all references
通過這個圖
查看(內存泄漏)
該內存還被誰所引用,爲何還不能釋放
MAT基礎介紹來自Gracker
三、內存問題總覽
內存泄漏
非靜態內部類的靜態實例容易造成內存泄漏
非靜態內部類的存活需要依賴外部類Activity使用靜態成員(靜態成員引用Drawable、Bitmap等大內存對象)
使用handler時的內存問題
因爲Handler的非即時性,導致部分代碼不能及時釋放
可以使用Badoo開發的第三方的 WeakHandler註冊某個對象後未反註冊
註冊廣播接收器、註冊觀察者等等集合中對象沒清理造成的內存泄露
資源對象沒關閉造成的內存泄露
資源性對象比如(Cursor,File文件等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收內存。
一些不良代碼成內存壓力
- 循環以及遞歸
- 數據隨意申請大小
- 如果沒有用到不要定義全局變量
Bitmap使用不當
- 及時的銷燬
雖然,系統能夠確認Bitmap分配的內存最終會被銷燬,但是由於它佔用的內存過多,所以很可能會超過Java堆的限制。因此,在用完Bitmap時,要
及時的recycle掉
。recycle並不能確定立即就會將Bitmap釋放掉,但是會給虛擬機一個暗示:“該圖片可以釋放了”。設置一定的採樣率(二次採樣)
有時候,我們要顯示的區域很小,沒有必要將整個圖片都加載出來,而只需要記載一個縮小過的圖片,這時候可以設置一定的採樣率,那麼就可以大大減小佔用的內存。巧妙的運用軟引用(SoftRefrence)
有些時候,我們使用Bitmap後沒有保留對它的引用,因此就無法調用Recycle函數。這時候巧妙的運用軟引用,可以使Bitmap在內存快不足時得到有效的釋放
目前但凡是個圖片加載框架都會使用SoftRefrence
構造Adapter時,沒有使用緩存的 convertView
頻繁的方法中創建對象
不要在經常調用的方法中創建對象,尤其是忌諱在循環中創建對象。可以適當的使用 hashtable , vector 創建一組對象容器,然後從容器中去取那些對象,而不用每次 new 之後又丟棄。
四、 MAT內存分析
使用Dominator Tree -> Path To GC Roots –> with all references
上面MAT基礎裏面已經講根據某種類型的對象個數來分析內存泄漏。
Actions -> Histogram
上圖展示了內存中各種類型的對象個數和Shallow heap,我們看到byte[]佔用Shallow heap最多,那是因爲Honeycomb之後Bitmap Pixel Data的內存分配在Dalvik heap中。右鍵選中byte[]數組,選擇List Objects -> with incomingreferences,可以看到byte[]具體的對象列表:
我們發現第二個byte[]的Retained heap較大,內存泄漏的可能性較大,因此右鍵選中這行,Path To GC Roots -> exclude weak references,同樣可以看到上文所提到的情況,我們的Bitmap對象被leak所引用到,這裏存在着內存泄漏。
- 講的是對象及其應用的內存大小
- 講的是大型對象被誰所引用
內存分析工具還有一個比較流行的內存泄漏檢測庫:LeakCannary