Android系統(tǒng)的內(nèi)存與崩潰優(yōu)化

Android優(yōu)化

題記:當你看到一堆讓你摸不著頭腦的崩潰堆棧,夾雜著若干OOM崩潰的時候,那就是在告訴你——是時候優(yōu)化內(nèi)存了。

最近這段時間一直在跟進安卓崩潰的問題,跟了有三個月了,雖然有一些進展,但是目前也還沒有徹底解決,想起自己已經(jīng)有一年沒有寫過博客了,所以打算把最近學到的東西,都好好整理一下。

看到這個標題,就能知道我最近解決的崩潰問題都是內(nèi)存導致的了,內(nèi)存問題相比于普通的崩潰難度要高一些,因為內(nèi)存崩潰的堆棧都不能直接反饋問題,有些只是壓死駱駝的最后一根稻草,崩潰點也是五花八門。所以要解決內(nèi)存的問題,首先就要對內(nèi)存有一個全面切清晰的認識,你才知道要從哪里入手。

內(nèi)存的相關概念——弄懂大Boss

  • 虛擬內(nèi)存和物理內(nèi)存

    物理內(nèi)存,顧名思義,就是實際分配到內(nèi)存條中的內(nèi)存;虛擬內(nèi)存,也稱邏輯內(nèi)存,是存在于數(shù)據(jù)結構中的。在32位操作系統(tǒng)中,Linux進程的虛擬內(nèi)存大小是4G,32位系統(tǒng)的最大尋址空間大小正好是4G(2^32)。在這4G里面,其中1G是內(nèi)核態(tài)內(nèi)存,每個Linux進程共享這一塊內(nèi)存,在高地址;3G是用戶態(tài)內(nèi)存,這部分內(nèi)存進程間不共享,在低地址。內(nèi)存的分布圖如下:

而用戶態(tài)又被切分為很多個部分,從低地址往上分別是:

  1. Text Segment :存放二進制可執(zhí)行代碼的位置;

  2. Data Segment: 存放靜態(tài)常量;

  3. BSS Segment :存放未初始化的靜態(tài)變量;

  4. Heap:堆是往高地址增長的,是用來動態(tài)分配內(nèi)存的區(qū)域,malloc 就是在這里面分配的;

  5. Memory Mapping Segment:這塊地址可以用來把文件映射進內(nèi)存用的,如果二進制的執(zhí)行文件依賴于某個動態(tài)鏈接庫,就是在這個區(qū)域里面將 so 文件映射到了內(nèi)存中;

  6. Stack:主線程的函數(shù)調(diào)用的函數(shù)棧就是用這里的。

    虛擬內(nèi)存的分布,在進程中是有數(shù)據(jù)結構來記錄的,所有進程內(nèi)申請內(nèi)存的動作,無論是java的new,還是c++的malloc,首先都是申請的虛擬內(nèi)存,要等到實際發(fā)生內(nèi)存訪問的時候,會發(fā)生缺頁中斷,然后在物理內(nèi)存中分配內(nèi)存。

這個步驟比較繁瑣,涉及的知識點比較多,后面我會整理這中間學到的Linux內(nèi)存相關知識,再給大家分享。

  • Dalvik內(nèi)存和native內(nèi)存

    要搞清楚這兩個概念,首先要搞清楚Dalvik和Linux的關系。也就是說,每個安卓上的進程,都會啟動一個Dalvik虛擬機,Java申請的內(nèi)存,就是Linux進程可以分配給Dalvik虛擬機的內(nèi)存,安卓系統(tǒng)內(nèi)有一項配置:dalvik.vm.heapsize,定義了這臺手機上每個dalvik能申請的最大內(nèi)存,也就是系統(tǒng)能給虛擬機分配的最大內(nèi)存。高端的手機一般能分配到512M的最大內(nèi)存,低端機一般是256M。

    而Native內(nèi)存,是脫離于Dalvik虛擬機,直接在系統(tǒng)層面申請的內(nèi)存,只要虛擬內(nèi)存和物理內(nèi)存沒用完,就能一直申請,這也是為什么,android 4.x之后,Bitmap的內(nèi)存會直接在native中分配的緣故,就是因為看上了Native比Dalvik大。

  • VSS, USS, PSS和RSS

這幾個概念,是另外一套用來描述進程內(nèi)存現(xiàn)狀的,要理解這幾個概念,首先要理解系統(tǒng)內(nèi)存的分配方式,也就是虛擬內(nèi)存是通過什么樣的機制分配到物理內(nèi)存中去的。在android系統(tǒng)中,使用的分配方式是分頁,這也是Linux內(nèi)最常用的分配內(nèi)存方式。

簡單來說,分頁就是將系統(tǒng)內(nèi)存分成一頁頁,每頁的內(nèi)存大小是4K,然后系統(tǒng)會跟蹤所有的內(nèi)存頁面,如下圖:

image.png

在確定應用使用的內(nèi)存量時,系統(tǒng)必須考慮共享的頁面。訪問相同服務或庫的應用將共享內(nèi)存頁面。例如,Google Play 服務和某個游戲應用可能會共享位置信息服務。這樣便很難確定屬于整個服務和每個應用的內(nèi)存量分別是多少。

image.png

圖 6. 由兩個應用共享的頁面(中間)

如需確定應用的內(nèi)存占用量,可以使用以下任一指標:

  1. 常駐內(nèi)存大小 (RSS):應用使用的共享和非共享頁面的數(shù)量
  2. 按比例分攤的內(nèi)存大小 (PSS):應用使用的非共享頁面的數(shù)量加上共享頁面的均勻分攤數(shù)量(例如,如果三個進程共享 3MB,則每個進程的 PSS 為 1MB)
  3. 獨占內(nèi)存大小 (USS):應用使用的非共享頁面數(shù)量(不包括共享頁面)

如果操作系統(tǒng)想要知道所有進程使用了多少內(nèi)存,那么 PSS 非常有用,因為頁面只會統(tǒng)計一次。計算 PSS 需要花很長時間,因為系統(tǒng)需要確定共享的頁面以及共享頁面的進程數(shù)量。RSS 不區(qū)分共享和非共享頁面(因此計算起來更快),更適合跟蹤內(nèi)存分配量的變化。

查看PSS的方式比較簡單,安卓有直接的adb命令可以查看,命令如下:

dengzongrongdeMacBook-Pro-3:~ RoyDeng$ adb shell dumpsys meminfo com.dianyun.pcgo
Applications Memory Usage (in Kilobytes):
Uptime: 407897842 Realtime: 816330794

** MEMINFO in pid 11779 [com.dianyun.pcgo] **
                   Pss  Private  Private  SwapPss     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
  Native Heap   120337   120164        0      298   157172   148680     8491
  Dalvik Heap    32207    32164        0       57    41412    20706    20706
 Dalvik Other     8297     8296        0        0
        Stack       84       84        0        0
       Ashmem      158       72        0        0
      Gfx dev    26632    26632        0        0
    Other dev      204        0      204        0
     .so mmap    68401     3140    54180       12
    .jar mmap     4384        0     1220        0
    .apk mmap     2751      152     1516        0
    .ttf mmap     4402        0     1172        0
    .dex mmap    66430    55992    10096        0
    .oat mmap     2480        0      516        0
    .art mmap     5450     4680        0       33
   Other mmap    12593     1396     7544        0
      Unknown    12874    12852        0        8
        TOTAL   368092   265624    76448      408   198584   169386    29197

 App Summary
                       Pss(KB)
                        ------
           Java Heap:    36844
         Native Heap:   120164
                Code:   127984
               Stack:       84
            Graphics:    26632
       Private Other:    30364
              System:    26020

               TOTAL:   368092       TOTAL SWAP PSS:      408
  • 查看進程內(nèi)存分布

Linux將進程的內(nèi)存分布寫在一個文件里面,可以通過以下命令查看內(nèi)存情況:

$ cat /proc/[pid]/maps

其中vm_size這一行,就是表示進程的虛擬內(nèi)存大小,這個數(shù)據(jù)目前只能通過讀取/proc/[pid]/maps文件來獲得,是我們在內(nèi)存優(yōu)化中,非常重要的一項指標。

OOM崩潰——第一只攔路虎

說到內(nèi)存問題,第一想到的應該就是OOM崩潰了。以前我碰到OOM問題都會選擇性過濾,認為這個崩潰沒那么好解,要等到后續(xù)專門針對內(nèi)存做統(tǒng)一優(yōu)化;現(xiàn)在,能碰到OOM,真是一種幸福??。

我們碰到的OOM崩潰主要有以下兩種:

  1. 832503 java.lang.OutOfMemoryError: Failed to allocate a 9936012 byte allocation with 4745096 free bytes and 4MB until OOM

  2. 269867 java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again

第一種崩潰比較簡單,原因就是給dalvik分配的最大內(nèi)存超過限制了,這塊內(nèi)存分配不了,然后系統(tǒng)就報錯了。解法有兩種:

  1. 簡單解法:一般報錯的地方都是申請內(nèi)存比較頻繁或者比較大的地方,可以直面這個堆棧,想辦法繞過去,比如說把一張大圖用.9圖來代替等等。

    不過這個方法有個壞處,就是不徹底,因為有可能堆棧反應的問題,只是壓死駱駝的最后一根稻草,要想徹底解決這個問題,安卓提供了一個非常好的工具,就是hprof。

  2. 徹底解法:就是想辦法拿到崩潰場景下的hprof文件,這樣就能看到崩潰前到底是什么東西占用著內(nèi)存。因為導出hprof需要時間,并且dump的過程中app會被凍結,所以在用戶環(huán)境上不是很好導出。最好的方法,是觀察用戶行為日志,一邊借助內(nèi)存檢測工具(我們使用的是perfDog),來重現(xiàn)崩潰場景。

    快手最近有開源一款解決dump過程中app凍結的內(nèi)存導出方案——KOOM,我們還沒有在項目中實際投入使用,有興趣的可以了解一下:https://github.com/KwaiAppTeam/KOOM

第二種崩潰,是java在創(chuàng)建線程的時候發(fā)生的崩潰。從堆??梢钥闯觯罎Ⅻc發(fā)生在native層,所以和虛擬機內(nèi)存限制無關。通過在網(wǎng)上查資料——《Android 創(chuàng)建線程源碼與OOM分析》,發(fā)現(xiàn)出現(xiàn)這個問題的原因有兩種,一種是虛擬內(nèi)存不足,第二種是文件描述符超出限制,究竟是因為哪個呢?

為了定位這個問題,我們做了一套崩潰收集方案,目的不是為了收集崩潰,而是收集崩潰時候的機器狀態(tài),包括內(nèi)存、線程、文件描述符、activityStack等等,在灰度的過程中,我們也在看其他的那些比oom更加棘手的問題。

亂七八糟的Native崩潰——真正的大Boss

OOM問題,有簡單的,有復雜的,不過真正把我們難倒的,不是OOM,而是一堆亂七八糟的native崩潰,其中TOP1崩潰的堆棧,看上去很短,但是很讓人摸不著頭腦:

1   #00 pc 00056c62 /apex/com.android.runtime/lib/bionic/libc.so (abort+165) [armeabi-v8]
2   #01 pc 00005aad /system/lib/liblog.so (__android_log_assert+176) [armeabi-v8]
3   #02 pc 001f78f7 /system/lib/libhwui.so [armeabi-v8]
4   #03 pc 001f6abd /system/lib/libhwui.so [armeabi-v8]
5   #04 pc 001f6011 /system/lib/libhwui.so [armeabi-v8]
6   #05 pc 002041e9 /system/lib/libhwui.so [armeabi-v8]
7   #06 pc 00204041 /system/lib/libhwui.so [armeabi-v8]
8   #07 pc 0000da1f /system/lib/libutils.so (android::Thread::_threadLoop(void*)+214) [armeabi-v8]
9   #08 pc 000a109b /apex/com.android.runtime/lib/bionic/libc.so (__pthread_start(void*)+20) [armeabi-v8]
10 #09 pc 00058113 /apex/com.android.runtime/lib/bionic/libc.so (__start_thread+30) [armeabi-v8]

崩潰的原因是abort,一般出現(xiàn)這種問題的原因,是系統(tǒng)沒辦法了,然后自殺了,而這個沒辦法的原因,讓我猜到了和內(nèi)存可能有關,但是不能確定。通過客戶端收集到的log,也沒辦法定位到問題,因為客戶端只收集自己打印的log。眼看就要沒辦法之際,一次偶然的機會,看到了bugly上收集到的短暫的logcat日志,竟然每一條都有這樣同樣的log輸出:

10-24 00:05:20.046 13795 14043 W Adreno-GSL: <sharedmem_gpuobj_alloc:2713>: sharedmem_gpumem_alloc: mmap failed errno 12 Out of memory
10-24 00:05:20.048 13795 14043 E Adreno-GSL: <gsl_memory_alloc_pure:2297>: GSL MEM ERROR: kgsl_sharedmem_alloc ioctl failed.
10-24 00:05:20.055 13795 14043 W Adreno-GSL: <sharedmem_gpuobj_alloc:2713>: sharedmem_gpumem_alloc: mmap failed errno 12 Out of memory
10-24 00:05:20.056 13795 14043 E Adreno-GSL: <gsl_memory_alloc_pure:2297>: GSL MEM ERROR: kgsl_sharedmem_alloc ioctl failed.
10-24 00:05:20.057 13795 14043 W Adreno-GSL: <sharedmem_gpuobj_alloc:2713>: sharedmem_gpumem_alloc: mmap failed errno 12 Out of memory
10-24 00:05:20.058 13795 14043 E Adreno-GSL: <gsl_memory_alloc_pure:2297>: GSL MEM ERROR: kgsl_sharedmem_alloc ioctl failed.
10-24 00:05:20.059 13795 14043 E OpenGLRenderer: GL error: Out of memory!

得嘞,問題就是你了,GL error:Out of Memory,對應的代碼是在這里:

bool GLUtils::dumpGLErrors() {
    bool errorObserved = false;
    GLenum status = GL_NO_ERROR;
    while ((status = glGetError()) != GL_NO_ERROR) {
        errorObserved = true;
        switch (status) {
            case GL_INVALID_ENUM:
                ALOGE("GL error:  GL_INVALID_ENUM");
                break;
            case GL_INVALID_VALUE:
                ALOGE("GL error:  GL_INVALID_VALUE");
                break;
            case GL_INVALID_OPERATION:
                ALOGE("GL error:  GL_INVALID_OPERATION");
                break;
            case GL_OUT_OF_MEMORY:
                ALOGE("GL error:  Out of memory!");
                break;
            default:
                ALOGE("GL error: 0x%x", status);
        }
    }
    return errorObserved;
}

從上面的創(chuàng)建線程OOM和GL OOM兩個問題,已經(jīng)大概猜到這次碰到的問題就是內(nèi)存問題了,但是要證明是內(nèi)存問題,還是需要有證據(jù)。對此,我們的方法是在崩潰回調(diào)的接口中,搜集當時的數(shù)據(jù),包括java內(nèi)存、native內(nèi)存、虛擬內(nèi)存、文件描述符、線程數(shù)等等,搜集到的數(shù)據(jù)會在log中打印,并且上報給崩潰后臺,通過收集一輪數(shù)據(jù),我們發(fā)現(xiàn)我們的猜想是正確的:崩潰的原因,就是虛擬內(nèi)存不足導致,同文件描述符、線程數(shù)、java內(nèi)存大小等其他指標無關。

GL OOM——挑戰(zhàn)大Boss

不管是虛擬內(nèi)存還是物理內(nèi)存,要解決內(nèi)存問題的思路都是一樣的:首先就是要找到問題出在哪,只要找到了問題根源,解決問題就不是一件麻煩事。但是問題就在于,怎么樣才能找到內(nèi)存問題的大頭在哪呢?為此,我們做了一套比較完整的內(nèi)存交控方案,具體策略如下:

  1. 在Activity跳轉的過程中添加內(nèi)存信息打印,打印的時間點包括:

    1. Activity.onStart()
    2. Activity.onStop()
    3. 在同一個Activity的停留時間每超過一分鐘

    這樣我們就得到了一份內(nèi)存數(shù)據(jù)增長曲線,通過收集內(nèi)存問題崩潰用戶的log,最終定位到有三個Activity有嚴重的內(nèi)存泄露問題

  2. 我們碰到的內(nèi)存問題都是偶現(xiàn)問題,所以要想解決問題,同時驗證解決方案是否生效,必須要做的一件事情就是想辦法重現(xiàn)問題。所以我們開發(fā)同學配合測試同學,一起搭建了一套自動化測試框架,輔助性能分析工具perfDog,成功找出了問題所在。

找到了問題根源,解決問題就簡單很多,因為涉及到項目問題,在這里就不透露解決問題的方案。

問題的原因主要有兩點:

  1. 在我們的業(yè)務場景中,有一個場景,用戶會頻繁連接和斷開音視頻直播流,這個過程每發(fā)生一次,就會造成20M的內(nèi)存泄露。下面是自動化腳本模擬該業(yè)務場景的內(nèi)存增長曲線:


    重連直播流內(nèi)存泄漏
  1. X5框架內(nèi)存在內(nèi)存堆積,堆積點是用戶退出網(wǎng)頁之后,jsapi會繼續(xù)持有X5WebViewAdapter,繼而持有Activity,導致內(nèi)存堆積,堆棧如下:


    X5內(nèi)存泄露

知道了上面兩個問題,解決方案就可以順藤摸瓜了:

  1. 直播流內(nèi)存泄露問題,找到了原因是很多析構函數(shù)沒有找到,問題已修復;
  2. web內(nèi)存堆積問題,解決方案是做web子進程改造,這也是微信采用的解決方案,目前正在開發(fā)中。

經(jīng)驗分享

經(jīng)過這次崩潰優(yōu)化,自己總結了一些方法論,如果要解決內(nèi)存相關的崩潰問題,要做的事情分三步:

  1. 確認崩潰和內(nèi)存相關。這一步需要開發(fā)者有扎實的理論功底,能夠理解安卓內(nèi)存模型,并且要通過分析崩潰堆棧,得到該崩潰是否是內(nèi)存問題的結論。
  2. 找到內(nèi)存問題原因。這一步需要開發(fā)者有偵察能力和足夠的耐心,能夠通過log發(fā)現(xiàn)用戶操作的共性,然后謹慎的重現(xiàn)問題,只要重現(xiàn)了問題,后面的事情就好辦了。這一步也是最重要的一步。
  3. 優(yōu)化內(nèi)存問題點。這一步需要開發(fā)者有豐富的開發(fā)經(jīng)驗,要得出最小成本的解決方案。

以上就是我在最近崩潰優(yōu)化中所總結出來的一些經(jīng)驗,感謝您的閱讀,希望能對你有幫助,有問題歡迎留言討論!

相關鏈接:
native 內(nèi)存和 dalvik內(nèi)存
進程間的內(nèi)存分配
KOOM——高性能線上內(nèi)存監(jiān)控方案
Android 創(chuàng)建線程源碼與OOM分析

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容