Android 記憶體詳細分析
這是一篇很不錯的文章,作者分析的比較深入。尊重原創作者,轉載請註明出處:
最近在網上看了不少Android記憶體管理方面的博文,但是文章大多都是就單個方面去介紹記憶體管理,沒有能全域性把握,缺乏系統性闡述,而且有些觀點有誤。
這樣對Android記憶體管理進行區域性性介紹,很難使讀者建立系統性概念,無法真正理解記憶體管理,對提高系統優化和系統穩定性分析方面的能力是不夠的。
我結合自己的一些思考和理解,從巨集觀層面上,對記憶體管理做一個全域性性的介紹,在此與大家交流分享。
首先,回顧一下基礎知識,基礎知識是理解系統機制的前提和關鍵:
1、 程序的地址空間
在32位作業系統中,程序的地址空間為0到4GB,
示意圖如下:
圖1
這裡主要說明一下Stack和Heap:
Stack空間(進棧和出棧)由作業系統控制,其中主要儲存函式地址、函式引數、區域性變數等等,所以Stack空間不需要很大,一般為幾MB大小。
Heap空間的使用由程式設計師控制,程式設計師可以使用malloc、new、free、delete等函式呼叫來操作這片地址空間。Heap為程式完成各種複雜任務提供記憶體空間,所以空間比較大,一般為幾百MB到幾GB。正是因為Heap空間由程式設計師管理,所以容易出現使用不當導致嚴重問題。
2、程序記憶體空間和RAM之間的關係
程序的記憶體空間只是虛擬記憶體(或者叫作邏輯記憶體),而程式的執行需要的是實實在在的記憶體,即實體記憶體(RAM)。在必要時,作業系統會將程式執行中申請的記憶體(虛擬記憶體)對映到RAM,讓程序能夠使用實體記憶體。
RAM作為程序執行不可或缺的資源,對系統性能和穩定性有著決定性影響。另外,RAM的一部分被作業系統留作他用,比如視訊記憶體等等,記憶體對映和視訊記憶體等都是由作業系統控制,我們也不必過多地關注它,程序所操作的空間都是虛擬地址空間,無法直接操作RAM。
示意圖如下:
圖2
基礎知識介紹到這裡,如果讀者理解以上知識有障礙,請好好惡補一下基礎知識,基礎理論知識至關重要。3、 Android中的程序
(1) native程序:採用C/C++實現,不包含dalvik例項的程序,/system/bin/目錄下面的程式檔案執行後都是以native程序形式存在的。如圖 3,/system/bin/surfaceflinger、/system/bin/rild、procrank等就是native程序。
(2) java程序:Android中運行於dalvik虛擬機器之上的程序。dalvik虛擬機器的宿主程序由fork()系統呼叫建立,所以每一個java程序都是存在於一個native程序中,因此,java程序的記憶體分配比native程序複雜,因為程序中存在一個虛擬機器例項。如圖3,Android系統中的應用程式基本都是java程序,如桌面、電話、聯絡人、狀態列等等。
圖3
4、 Android中程序的堆記憶體
圖1和圖4分別介紹了native process和java process的結構,這個是我們程式設計師需要深刻理解的,程序空間中的heap空間是我們需要重點關注的。heap空間完全由程式設計師控制,我們使用的malloc、C++ new和java new所申請的空間都是heap空間, C/C++申請的記憶體空間在native heap中,而java申請的記憶體空間則在dalvik heap中。
圖4
5、 Android的 java程式為什麼容易出現OOM
這個是因為Android系統對dalvik的vm heapsize作了硬性限制,當java程序申請的java空間超過閾值時,就會丟擲OOM異常(這個閾值可以是48M、24M、16M等,視機型而定),可以通過adb shell getprop | grep dalvik.vm.heapgrowthlimit檢視此值。
也就是說,程式發生OMM並不表示RAM不足,而是因為程式申請的java heap物件超過了dalvik vm heapgrowthlimit。也就是說,在RAM充足的情況下,也可能發生OOM。
這樣的設計似乎有些不合理,但是Google為什麼這樣做呢?這樣設計的目的是為了讓Android系統能同時讓比較多的程序常駐記憶體,這樣程式啟動時就不用每次都重新載入到記憶體,能夠給使用者更快的響應。迫使每個應用程式使用較小的記憶體,移動裝置非常有限的RAM就能使比較多的app常駐其中。但是有一些大型應用程式是無法忍受vm heapgrowthlimit的限制的,後面會介紹如何讓自己的程式跳出vm heapgrowthlimit的限制。
6、 Android如何應對RAM不足
在第5點中提到:java程式發生OMM並不是表示RAM不足,如果RAM真的不足,會發生什麼呢?這時Android的memory killer會起作用,當RAM所剩不多時,memory killer會殺死一些優先順序比較低的程序來釋放實體記憶體,讓高優先順序程式得到更多的記憶體。我們在分析log時,看到的程序被殺的log,如圖5,往往就是屬於這種情況。
圖5
7、 如何檢視RAM使用情況
可以使用adb shell cat /proc/meminfo檢視RAM使用情況:
MemTotal: 396708 kB
MemFree: 4088 kB
Buffers: 5212 kB
Cached: 211164 kB
SwapCached: 0 kB
Active: 165984 kB
Inactive: 193084 kB
Active(anon): 145444 kB
Inactive(anon): 248 kB
Active(file): 20540 kB
Inactive(file): 192836 kB
Unevictable: 2716 kB
Mlocked: 0 kB
HighTotal: 0 kB
HighFree: 0 kB
LowTotal: 396708 kB
LowFree: 4088 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 145424 kB
……
……
這裡對其中的一些欄位進行解釋:
MemTotal:可以使用的RAM總和(小於實際RAM,作業系統預留了一部分)
MemFree:未使用的RAM
Cached:快取(這個也是app可以申請到的記憶體)
HightTotal:RAM中地址高於860M的實體記憶體總和,只能被使用者空間的程式使用。
HightFree:RAM中地址高於860M的未使用記憶體
LowTotal:RAM中核心和使用者空間程式都可以使用的記憶體總和(對於512M的RAM: lowTotal= MemTotal)
LowFree: RAM中核心和使用者空間程式未使用的記憶體(對於512M的RAM: lowFree = MemFree)
8、 如何檢視程序的記憶體資訊
(1)、使用adb shell dumpsys meminfo + packagename/pid:
從圖6可以看出,com.example.demo作為java程序有2個heap,native heap和dalvik heap,
native heap size為159508KB,dalvik heap size為46147KB
圖6
(2)、使用adb shell procrank檢視程序記憶體資訊
如圖7:
圖7
解釋一些欄位的意思:
VSS- Virtual Set Size 虛擬耗用記憶體(包含共享庫佔用的記憶體)
RSS- Resident Set Size 實際使用實體記憶體(包含共享庫佔用的記憶體)
PSS- Proportional Set Size 實際使用的實體記憶體(比例分配共享庫佔用的記憶體)
USS- Unique Set Size 程序獨自佔用的實體記憶體(不包含共享庫佔用的記憶體)
一般來說記憶體佔用大小有如下規律:VSS >= RSS >= PSS >= USS
注意:dumpsys meminfo可以檢視native程序和java程序,而procrank只能檢視java程序。
9、 應用程式如何繞過dalvikvm heapsize的限制
對於一些大型的應用程式(比如遊戲),記憶體使用會比較多,很容易超超出vm heapsize的限制,這時怎麼保證程式不會因為OOM而崩潰呢?
(1)、建立子程序
建立一個新的程序,那麼我們就可以把一些物件分配到新程序的heap上了,從而達到一個應用程式使用更多的記憶體的目的,當然,建立子程序會增加系統開銷,而且並不是所有應用程式都適合這樣做,視需求而定。
建立子程序的方法:使用android:process標籤
(2)、使用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殺掉了。
(3)、使用視訊記憶體(作業系統預留RAM的一部分作為視訊記憶體)
使用OpenGL textures等API,texture memory不受dalvik vm heapsize限制,這個我沒有實踐過。再比如Android中的GraphicBufferAllocator申請的記憶體就是視訊記憶體。
10、Bitmap分配在native heap還是dalvik heap上?
一種流行的觀點是這樣的:
Bitmap是jni層建立的,所以它應該是分配到native heap上,並且為了解釋bitmap容易導致OOM,提出了這樣的觀點:
native heap size + dalvik heapsize <= dalvik vm heapsize
但是請大家看看圖6,native heap size為159508KB,遠遠超過dalvik vm heapsize,所以,事實證明以上觀點是不正確的。
正確的觀點:
大家都知道,過多地建立bitmap會導致OOM異常,且native heapsize不受dalvik限制,所以可以得出結論:
Bitmap只能是分配在dalvik heap上的,因為只有這樣才能解釋bitmap容易導致OOM。
但是,有人可能會說,Bitmap確實是使用java native方法建立的啊,為什麼會分配到dalvik heap中呢?為了解決這個疑問,我們還是分析一下原始碼:
涉及的檔案:
- framework/base/graphic/java/Android/graphics/BitmapFactory.java
- framework/base/core/jni/Android/graphics/BitmapFactory.cpp
- framework/base/core/jni/Android/graphics/Graphics.cpp
BitmapFactory.java裡面有幾個decode***方法用來建立bitmap,最終都會呼叫:
private staticnative Bitmap nativeDecodeStream(InputStream is, byte[] storage,Rect padding,Options opts);
而nativeDecodeStream()會呼叫到BitmapFactory.cpp中的deDecode方法,最終會呼叫到Graphics.cpp的createBitmap方法。
我們來看看createBitmap方法的實現:
- jobjectGraphicsJNI::createBitmap(JNIEnv* env, SkBitmap* bitmap, jbyteArray buffer,
- boolisMutable, jbyteArray ninepatch, int density)
- {
- SkASSERT(bitmap);
- SkASSERT(bitmap->pixelRef());
- jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
- static_cast<jint>(reinterpret_cast<uintptr_t>(bitmap)),
- buffer, isMutable, ninepatch,density);
- hasException(env); // For the side effectof logging.
- return obj;
- }
從程式碼中可以看到bitmap物件是通過env->NewOject( )建立的,到這裡疑惑就解開了,bitmap物件是虛擬機器建立的,JNIEnv的NewOject方法返回的是java物件,並不是native物件,所以它會分配到dalvik heap中。
11、java程式如何才能建立native物件
必須使用jni,而且應該用C語言的malloc或者C++的new關鍵字。例項程式碼如下:
- JNIEXPORT void JNICALLJava_com_example_demo_TestMemory_nativeMalloc(JNIEnv *, jobject)
- {
- void * p= malloc(1024*1024*50);
- SLOGD("allocate50M Bytes memory");
- if (p !=NULL)
- {
- //memorywill not used without calling memset()
- memset(p,0, 1024*1024*50);
- }
- else
- SLOGE("mallocfailure.");
- ….
- ….
- free(p); //free memory
- }
或者:
- JNIEXPORT voidJNICALL Java_com_example_demo_TestMemory_nativeMalloc(JNIEnv *, jobject)
- {
- SLOGD("allocate 50M Bytesmemory");
- char *p = newchar[1024 * 1024 * 50];
- if (p != NULL)
- {
- //memory will not usedwithout calling memset()
- memset(p, 1, 1024*1024*50);
- }
- else
- SLOGE("newobject failure.");
- ….
- ….
- free(p); //free memory
- }
這裡對程式碼中的memset做一點說明:
new或者malloc申請的記憶體是虛擬記憶體,申請之後不會立即對映到實體記憶體,即不會佔用RAM,只有呼叫memset使用記憶體後,虛擬記憶體才會真正對映到RAM。
本文旨在讓大家對Android記憶體管理有一個整體性的認識,著重全域性性理解,希望對大家有用