1. 程式人生 > >Android程序的記憶體管理分析

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例項的linux程序,/system/bin/目錄下面的程式檔案執行後都是以native程序形式存在的。如圖           3,/system/bin/surfaceflinger、/system/bin/rild、procrank等就是native程序。

(2)   java程序:例項化了dalvik虛擬機器例項的linux程序,程序的入口main函式為java函式。dalvik虛擬機器例項的宿主程序是fork()系統呼叫建立的linux程序,所以每一個android上的java程序實際上就是一個linux程序,只是程序中多了一個dalvik虛擬機器例項。因此,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 texturesAPItexture 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 = new char[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記憶體管理有一個整體性的認識,著重全域性性理解,希望對大家有用。

如果對java層記憶體洩漏感興趣,可以閱讀我的文章 Android記憶體洩漏分析及除錯