Android Memory(三) -- 問題原因分析

語言: CN / TW / HK

highlight: a11y-dark

Android-Go.jpeg

前言

     在工作這幾年,我一直深受記憶體問題的困擾,在和記憶體的不斷抗爭中,我逐漸積累了一些記憶體的知識,接來下來我會用幾篇文章簡單記錄一下這幾年的我學到的記憶體相關的經驗。另外,本系列文章不去過多的分析Linux底層程式碼,只是探討遇到記憶體問題時的解決方法論。 以下是全部文章的標題和連結: 1. Android Memory(一) -- 記憶體基礎知識 2. Android Memory(二) -- 應用記憶體佔用分析 3. Android Memory(三) -- 問題原因分析 4. Android Memory(四) -- 問題解決方案1 5. Android Memory(五) -- 問題解決方案2 6. Android Memory(六) -- 記憶體日常監控

通過前兩篇的文章,我們瞭解一些基本的Android知識,這一篇開始,我們這種分析應用的記憶體問題的定位、解決方向以及監控。

問題定位與分析

     應用釋出以後,突然之間線上出現了很多OOM,首先我們要知道問題的原因,在沒有定位出來原因之前,盲目的去做某一部分的優化,最終優化的結果會達不到我們的預期,並且費時費力。

     首先我們把線上OOM的情況大致列舉一下,大體上分為以下幾類:

  1. 程序虛擬記憶體不足
  2. Java堆記憶體不足
  3. 連續記憶體不足
  4. FD數量超過系統限制
  5. 執行緒數超過系統限制
  6. 手機實體記憶體不足
一、程序虛擬記憶體不足

     儘管2019年開始,Google Play就開始強制要求開發者上傳包含 64位架構支援的應用,保證應用執行在64位模式。但多年以來,大部分國內應用仍然執行在 32 位相容模式。並且有一部分裝置還對64位模式支援的不是很好。32位程序的虛擬地址空間是232=4G,隨著應用的功能不斷膨脹,各種框架、媒體播放器、WebView等能力被不斷新增進來,這時候你就會發現虛擬記憶體不夠用了,起碼我知道國內不少大型應用深受虛擬記憶體OOM的問題困擾。那麼虛擬記憶體不足都有一些什麼現象呢?

  1. 線上出現大量signal 6相關的Crash,點開以後也都是libc abort,並且Android 10的設定佔比最多

image.png

image.png

  1. 線上出現大量的pthread異常,並且提示資訊是Try again

image.png

  1. 線上還有一部分執行緒建立mmap失敗的異常

image.png

如果有這幾個現象的時候,您可能要懷疑是不是應用的虛擬記憶體不足了,此時如果您還不確定,可以自己在線上進行埋點佐證。

二、Java堆記憶體不足

這種問題一般都是記憶體洩露或者記憶體使用不合理導致,其有以下特徵:

image.png

image.png

這類問題比較多時,要去考慮記憶體洩露和不正常的記憶體使用問題了

三、連續記憶體不足

一般來說這種問題上報相對較少, 但是我還是列舉出來一些示例供大家參考: image.png

image.png

遇到這種問題,一般程序中存在大量的記憶體碎片。這時候就得去考慮如何優化記憶體分配問題了。

四、FD數量超過系統限制

FD超標的問題還是比較常見的,我們可以通過  /proc/pid/limits 來檢視描述著系統對對應程序的限制:

tapd_51005639_1646640833_95.png

Soft Limit 軟限制:系統資源的使用的上限值,應用自己可以改變,但是不能超過Hard Limit Hard Limit 硬限制:Root許可權可以更改,可以改小,但不能改大

FD超出限制的原因有一下幾種: 1. 檔案開啟過多或開啟以後沒有關閉 2. Resource資源開啟過多或者沒有關閉,這裡邊包含開啟Socket、流、CursorWindow等 3. 帶Looper的Thread,Looper的建立需要使用FD 4. 執行緒建立過多,執行緒建立需要使用FD 5. InputChannel洩露 常見的異常有以下幾種:

  • "Could not allocate JNI Env" 提示

java.lang.OutOfMemoryError: Could not allocate JNI Env at java.lang.Thread.nativeCreate(Native Method) at java.lang.Thread.start(Thread.java:730) at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:941) at java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1009) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1151) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607) at java.lang.Thread.run(Thread.java:761)

  • "Too many open files" 提示 java.net.ConnectException: failed to connect to 40.118.73.216 (port 443) after 10000ms: connect failed: EMFILE (Too many open files) at libcore.io.IoBridge.connect(IoBridge.java:124) at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:183) at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:456) at java.net.Socket.connect(Socket.java:882) at com.squareup.okhttp.internal.Platform$Android.connectSocket(Platform.java:190) ...

  • "Could not read input channel" 提示

java.lang.RuntimeException: Could not read input channel file descriptors from at android.view.InputChannel.nativeReadFromParcel(Native Method) at android.view.InputChannel.readFromParcel(InputChannel.java:148) at android.view.InputChannel$1.createFromParcel(InputChannel.java:39) at android.view.InputChannel$1.createFromParcel(InputChannel.java:37) at com.android.internal.view.InputBindResult.<init>(InputBindResult.java:68) ...

  • "Could not open input channel pair" 提示 java.lang.RuntimeException: Could not open input channel pair. status=-24 at android.view.InputChannel.nativeOpenInputChannelPair(Native Method) at android.view.InputChannel.openInputChannelPair(InputChannel.java:94) at com.android.server.wm.WindowState.openInputChannel(WindowState.java:2011) at com.android.server.wm.WindowManagerService.addWindow(WindowManagerService.java:1406) at com.android.server.wm.Session.addToDisplay(Session.java:197) ...

  • "Could not copy bitmap to parcel blob" 提示

java.lang.RuntimeException: Could not copy bitmap to parcel blob. at android.graphics.Bitmap.nativeWriteToParcel(Native Method) at android.graphics.Bitmap.writeToParcel(Bitmap.java:1553) at android.widget.RemoteViews$BitmapCache.writeBitmapsToParcel(RemoteViews.java:984) at android.widget.RemoteViews.writeToParcel(RemoteViews.java:2854) at android.widget.RemoteViews.clone(RemoteViews.java:1903) ...

還有一些其他的提示,就不一一列舉了。

五. 執行緒數超過系統限制

和FD洩露一樣,可以通過/proc/sys/kernel/threads-max來檢視。目前來說在一些低版本手機上是有限制的,很多高版本手機上的執行緒數量限制都是1W+,等於沒有了限制。 比如華為低版本手機上記憶體數量達到400+就會出現OOM:

java.lang.OutOfMemoryError pthread_create (1040KB stack) failed: Out of memory java.lang.Thread.nativeCreate(Native Method) java.lang.Thread.start(Thread.java:743) java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:941) java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1009) java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1151) java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607) java.lang.Thread.run(Thread.java:774)

要是不確定是不是執行緒數超標,可以做一個簡單的監控線上的執行緒數量情況,然後再下定論,去做執行緒數的收斂。

  1. 手機實體記憶體不足

    這種OOM和前邊幾種記憶體不足堆疊沒有明顯的區別,我也是在自己Crash時做的系統記憶體上報,在上報中才發現系統的可用記憶體不足,具體介面可以參考我寫的Android Memory(一)

總結

     記憶體優化費事費力,記憶體問題的定位經常也非常棘手,所以在出現記憶體問題是要先確定問題的原因,後邊解決記憶體問題時才能事半功倍。另外記憶體並不是用的越少越好,合理才是關鍵。但是很重要的一點是應用記憶體要像自己的房間一樣,經常清掃。如果記憶體問題堆積比較多,解決起來會非常麻煩,所以還是在平時在記憶體優化、監控上要多花費一些功夫。

參考文件:

  1. https://tech.meituan.com/2019/11/14/crash-oom-probe-practice.html
  2. https://www.jianshu.com/p/befd4b86cc42
  3. https://blog.csdn.net/CSDNno/article/details/121961814