設(shè)置
  • 日夜間
    隨系統(tǒng)
    淺色
    深色
  • 主題色

誰動了我的內(nèi)存,揭秘 OOM 崩潰下降 90% 的秘密

ByteCode 2022/10/17 18:50:10 責(zé)編:遠(yuǎn)生

本文來自微信公眾號:ByteCode (ID:ByteCode1024),作者:程序員 DHL

今天這篇文章主要介紹內(nèi)存相關(guān)的知識點,以及那些因素會導(dǎo)致 OOM 崩潰和相對應(yīng)的解決方案,所以通過這篇文章你將學(xué)習(xí)到以下內(nèi)容:

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

  • 32 位和 64 位設(shè)備可用虛擬內(nèi)存分別是多少

  • 為什么虛擬內(nèi)存不足主要發(fā)生在 32 位的設(shè)備上

  • 如何解決虛擬內(nèi)存不足的問題

  • App 啟動完成之后,虛擬內(nèi)存的分布

  • 如何解決 Java 堆內(nèi)存不足的問題

  • Java 堆上還有很多可用的內(nèi)存,為什么還會出現(xiàn) OOM

  • 做性能優(yōu)化時,需要關(guān)心那些指標(biāo)數(shù)據(jù)

不知道小伙伴們有沒有經(jīng)歷過,相同的優(yōu)化方案,A 應(yīng)用上線之后,崩潰率下降很多,但是 B 應(yīng)用上線只有一點點收益,每個優(yōu)化方案,在不同的 App 上所得到的優(yōu)化效果未必一樣,因為每個 App 在不同的國家和地區(qū)面對的用戶群體不一樣,因此機型也都不一樣,所以我們需要了解內(nèi)存相關(guān)的知識點,結(jié)合線上和線下數(shù)據(jù),對自己的 App 進(jìn)行歸因,對癥下藥,才能取得較大的收益。

內(nèi)存是極其稀缺的資源,不合理的使用會導(dǎo)致可用內(nèi)存越來越少,可能會引發(fā)卡頓、ANR、OOM 崩潰、Native 崩潰等等,嚴(yán)重影響用戶的體驗。所以當(dāng)我們在做性能優(yōu)化的時候,內(nèi)存優(yōu)化是非常重要的環(huán)節(jié)。

初期在做內(nèi)存優(yōu)化的時候,在我們的腦海里都會有一個潛意識「內(nèi)存占用越少越好」,在某些情況下是不對的。例如在高端機上我們可以多分配點內(nèi)存,可以提升用戶的體驗,但是在低端機上內(nèi)存本身就很小,所以我們應(yīng)盡量減少內(nèi)存的分配。例如針對損耗性能的動畫、特效等等,在低端機上是不是可以關(guān)掉,或者關(guān)掉硬件加速、采用其他的方案代替,這樣不僅可以減少崩潰,還可以減少卡頓,提高用戶體驗。

因為 Java 有自動回收機制,所以在開發(fā)過程中,很少有人會去關(guān)心內(nèi)存問題,在腦海中都會有一個潛意識 GC 會自動回收,所以用完不會主動釋放掉無用資源例如 Bitmap、動畫、播放器等等,等待 GC 來回收,在實際項目中,依賴 GC 是不可靠的。首先 GC 自動回收機制具有不確定性,GC 也分為了不同的類型,如果發(fā)生 Full GC 時,會觸發(fā) stop the world 事件,會使 App 變得更加嚴(yán)重。

另外 GC 的回收機制根據(jù)可達(dá)性分析算法判斷一個對象是否可以被回收,如果存在內(nèi)存泄露,GC 是不會回收這些資源的,逐漸累積,當(dāng)達(dá)到堆的內(nèi)存上限時,發(fā)生 OOM 崩潰了,所以你要保證自己不要寫出內(nèi)存泄露的代碼,以及團(tuán)隊其他人不要寫出內(nèi)存泄露的代碼,然而實際情況這是不可能的,所以依靠 GC 自動回收機制這種想法是不可靠的。雖然 Java 有內(nèi)存回收機制,但是我們應(yīng)該在腦海中保留內(nèi)存管理的意識,所以當(dāng)申請完內(nèi)存,退出或者不在使用時,及時釋放掉內(nèi)存。真正做到 用時分配,及時釋放。

可用內(nèi)存越來越少時,嚴(yán)重時會導(dǎo)致 OOM 崩潰,做過 OOM 優(yōu)化的朋友應(yīng)該會發(fā)現(xiàn),線上捕獲的大部分 OOM 崩潰堆棧,都是壓死駱駝的最后一根稻草,并不是問題的根本所在,所以我們需要對 OOM 崩潰進(jìn)行歸因,找到占用內(nèi)存的大頭。降低整機已使用的內(nèi)存,從而降低 OOM 崩潰,因此我大概分為了以下幾個方面。

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

  • 堆內(nèi)存

  • 堆內(nèi)存泄露,指的是在程序運行時,給對象分配的內(nèi)存,當(dāng)程序退出或者退出界面時,分配的內(nèi)存沒有釋放或者因為其他原因無法釋放

  • 資源泄露,比如 FD、socket、線程等等,這些在每個手機上都是有數(shù)量的限制,如果使用了不釋放,就會因為資源的耗盡而崩潰,我們在線上就出現(xiàn)過 FD 的泄露,導(dǎo)致崩潰率漲了 3 倍

  • 分配的內(nèi)存到達(dá) Java 堆的上限

  • 可用內(nèi)存很多,因為內(nèi)存碎片化,沒有足夠的連續(xù)段的空間分配

  • 對象的單次分配或者多次分配累計過大,例如在循環(huán)動畫中一直創(chuàng)建 Bitmap

  • Java 堆內(nèi)存溢出

  • 內(nèi)存泄露

  • FD 的數(shù)量超出當(dāng)前手機的閾值

  • 線程的數(shù)量超出當(dāng)前手機的閾值

其中 FD 和線程崩潰占比很低,因此這不是我們前期優(yōu)化的重點。這篇文章我們重點介紹 虛擬內(nèi)存和物理內(nèi)存,下篇文章將會介紹堆內(nèi)存,堆內(nèi)存是程序在運行過程中為對象分配內(nèi)存的區(qū)域,它也屬于虛擬內(nèi)存的范圍。

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

介紹虛擬內(nèi)存之前,我們需要先介紹物理內(nèi)存,物理內(nèi)存就是實實在在的內(nèi)存(即內(nèi)存條),如果應(yīng)用直接對物理內(nèi)存操作,會存在很多問題:

安全問題,應(yīng)用之間的內(nèi)存空間沒有隔離,會導(dǎo)致應(yīng)用 A 可以修改應(yīng)用 B 的內(nèi)存數(shù)據(jù),這是非常不安全的

內(nèi)存空間利用率低,應(yīng)用對內(nèi)存的使用會出現(xiàn)內(nèi)存碎片化的問題,即使還有很多內(nèi)存可以用,但是沒有足夠的連續(xù)段的內(nèi)存分配,而導(dǎo)致崩潰

效率低,多個應(yīng)用同時對物理內(nèi)存進(jìn)行讀取和寫入時,使用效率會非常低

為了解決上面的問題,我們需要為每個應(yīng)用分配 "中間內(nèi)存" 最終會映射到物理內(nèi)存上,這就是接下來要說的虛擬內(nèi)存。

操作系統(tǒng)會為每個應(yīng)用分配一個獨立的虛擬內(nèi)存,實現(xiàn)應(yīng)用間的內(nèi)存隔離,避免了應(yīng)用 A 修改應(yīng)用 B 的內(nèi)存數(shù)據(jù)的問題,虛擬內(nèi)存最終會映射到物理內(nèi)存上,當(dāng)應(yīng)用申請內(nèi)存時,得到的是虛擬內(nèi)存,只有真正執(zhí)行寫操作時,才會分配到物理內(nèi)存,好處是應(yīng)用可以使用連續(xù)的地址空間來訪問不連續(xù)的物理內(nèi)存。

每個應(yīng)用程序可使用的虛擬內(nèi)存大小受 CPU 位寬及內(nèi)核的限制。我們常說的 16 位 cpu,32 位 cpu,64 位 CPU,指的都是 CPU 的位寬,表示的是一次能夠處理的數(shù)據(jù)寬度,即 CPU 能處理的 2 進(jìn)制位數(shù),即分別是 16bit,32bit 和 64bit。而目前市面上常用的是 32 位和 64 的設(shè)備。

32 位和 64 位設(shè)備可用虛擬內(nèi)存分別是多少

32 位設(shè)備可以使用的虛擬內(nèi)存大小 3GB

32 位 CPU 架構(gòu)的設(shè)備可使用的地址空間大小為 2^32=4GB, 虛擬內(nèi)存空間分為 內(nèi)核空間用戶空間,系統(tǒng)提供了三種虛擬地址空間分配的參數(shù),代表用戶空間可訪問的虛擬地址空間大小。

VMSPLIT_3G : 默認(rèn)值,表示用戶空間可使用 3GB 的低地址,剩下的 1GB 高地址分配給內(nèi)核

VMSPLIT_2G : 表示用戶空間可使用 2GB 的低地址

VMSPLIT_1G : 表示用戶空間可使用 1GB 的低地址

64 位應(yīng)用可以使用的虛擬內(nèi)存大小 512GB

64 位 CPU 架構(gòu)的設(shè)備雖然擁有 64 位的地址空間,但是不是全部都可以使用的,為了后期的擴(kuò)展,只能使用部分地址。

Android 默認(rèn)的虛擬地址的長度配置為 CONFIG_ARM64_VA_BITS=39,即 Android 的 64 位應(yīng)用可使用的地址空間大小為 2^39=512GB。

當(dāng) 32 位應(yīng)用在 64 位的設(shè)備上運行時,可使用 4GB 虛擬地址空間,而 64 位應(yīng)用可使用 512GB 的空間。因此在 64 位機器上不存在虛擬空間不足的問題。因此在 2019 年的時候 Google Play 要求除了提供 32 位的版本之外,還需要提供 64 位的版本。

在我們的 OOM 崩潰設(shè)備中,32 位的設(shè)備占比 50%+ 以上,虛擬內(nèi)存不足主要發(fā)生在 32 位的設(shè)備上。

為什么虛擬內(nèi)存不足主要發(fā)生在 32 位的設(shè)備上

在 32 位的設(shè)備上,受地址空間最大內(nèi)存 4 GB 限制,內(nèi)核空間占用 1G,剩下的 3G 是用戶空間,我們可以通過解析  /process/ pid / smaps 文件,查看當(dāng)前虛擬內(nèi)存分配情況。

https://android.googlesource.com/ platform / frameworks / base/+/3025ef332c29e255388f74b2afefe05f64bce07c / core / jni / android_os_Debug.cpp

系統(tǒng)資源預(yù)分配,包含了 Zygote 進(jìn)程初始化時,需要加載  Framework 層的代碼和資源。供 Fork 出來的子進(jìn)程可以直接使用。Framework 資源包含:Framework 層 Java 代碼、so、art 虛擬機、各種靜態(tài)資源字體、文件等等

系統(tǒng)預(yù)分配區(qū)域中其中 [anon:libwebview reservation] 區(qū)域占用 130MB 內(nèi)存

App 自身資源,包括 App 中的代碼、資源、 App 直接或者間接開啟線程消耗的棧空間、 App 申請的內(nèi)存、內(nèi)存文件映射等內(nèi)容。

Java 堆用于分配 Java / Kotlin 創(chuàng)建的對象。由 GC 管理和回收,GC 回收時將 From Space 里的對象復(fù)制到 To Space,這兩片區(qū)域分別為 dalvik-main space 和 dalvik-main space 1, 這兩片區(qū)域的大小和我當(dāng)前測試機 Java 堆大小一樣,都是 512 MB,如下圖所示

根據(jù) Android 源碼中的解釋,Java 堆的大小應(yīng)該是根據(jù) RAM Size 來設(shè)置的,這是一個經(jīng)驗值,廠商是可以更改的,如果手機 Root 之后,自己也可以改,無論 RAM 多大,到目前為止 Java 堆的上限默認(rèn)都是 512MB,Google 源碼的設(shè)置如下如下圖所示。https://android.googlesource.com/ platform / frameworks / native/+/master/ build

RAM (MB)-dalvik-heap.mkheapsize (MB)
phone-hdpi-dalvik-heap.mk32
512-dalvik-heap.mk128
1024-dalvik-heap.mk256
2048-dalvik-heap.mk512
4096-dalvik-heap.mk512
無論 RAM 多大,到目前為止堆的上限默認(rèn)都是 512MB

內(nèi)存文件映射,mmap 是一種內(nèi)存映射文件的方法,我們的 APK、Dex、so 等等都是通過 mmap 讀取的,會導(dǎo)致虛擬內(nèi)存增大,mmap 占用的內(nèi)存跟讀寫有關(guān)系

經(jīng)過分析內(nèi)核、系統(tǒng)資源、以及各 App 的資源占用,最后留給我們使用的內(nèi)存并不是很多,所以我們要合理使用系統(tǒng)資源,真正做到 "用時分配,及時釋放"。

如何解決虛擬內(nèi)存不足的問題

目前業(yè)界也有很多黑科技來釋放因系統(tǒng)占用的虛擬內(nèi)存不足的問題,大概有以下幾個方面的優(yōu)化。

Native 線程默認(rèn)的??臻g大小為 1M 左右,經(jīng)過測試大部分情況下線程內(nèi)執(zhí)行的邏輯并不需要這么大的空間,因此 Native 線程??臻g減半,可以減少 pthread_create OOM 崩潰

系統(tǒng)預(yù)分配區(qū)域中其中 [anon:libwebview reservation] 區(qū)域占用 130MB 內(nèi)存,可以嘗試釋放 WebView 預(yù)分配的內(nèi)存,減少一部分虛擬內(nèi)存

虛擬機堆空間減半,在上面提到過有兩片大小相同的區(qū)域分別 dalvik-main space 和 dalvik-main space 1,虛擬機堆空間減半其實就是減少其中一個 main space 所占用的內(nèi)存

快手針對垃圾回收器 jemalloc 的優(yōu)化,釋放的是 anon:libc_malloc 所占用的虛擬內(nèi)存

以下統(tǒng)計的是在 Android 7.0 App 首次啟動完成 libc_malloc 占用的虛擬內(nèi)存 156MB

Vss                      Pss                    Rss                                 name            
159744 kB           81789 kB                82320 kB                [anon:libc_malloc]

Android 11 之前使用的垃圾回收器是 jemalloc,Android 11 之后默認(rèn)使用的垃圾回收器是 scudo。

App 啟動完成之后,虛擬內(nèi)存的分布

下圖是 App 在 Android 7.0 上啟動完成之后所占用的虛擬內(nèi)存 (Vss),不同系統(tǒng)、不同的 App 虛擬內(nèi)存的分布都不一樣,我們可以通過解析  /process/ pid / smaps 文件,查看自己的 App 虛擬內(nèi)存分配情況。

https://android.googlesource.com/ platform / frameworks / base/+/3025ef332c29e255388f74b2afefe05f64bce07c / core / jni / android_os_Debug.cpp

正如上圖所示,主要分為三個部分:

dalvik(即 Java 堆),程序在運行過程中為對象分配內(nèi)存的區(qū)域

程序文件 dex 、 so 、 oat

Native

針對上面的問題,我們在項目中通過以下手段進(jìn)行優(yōu)化,重點優(yōu)化 dalvik 占用的內(nèi)存,因篇幅問題,將會在后面的文章中,做詳細(xì)的分析:

Android 3.0 ~ Android 7.0 上主要將 Bitmap 對象和像素數(shù)據(jù)統(tǒng)一放到 Java 堆中,Java 堆上限 512MB,而 Native 占用虛擬內(nèi)存,32 的設(shè)備可使用 3GB,64 位的設(shè)備更大,因此我們可以嘗試將 Bitmap 分配到 Native 上,緩解 Java 堆的壓力,降低 OOM 崩潰

使用第三方圖片庫時,需要針對高端機和低端機設(shè)置圖片庫不同的緩存大小,這樣我們在高端機上保證體驗的同時,降低低端機 OOM 崩潰率

收斂 Bitmap,避免重復(fù)創(chuàng)建 Bitmap,退出界面及時釋放掉資源(Bitmap、動畫、播放器等等資源)

內(nèi)存回收兜底策略,當(dāng) Activity 或者 Fragment 泄露時,與之相關(guān)聯(lián)的動畫、Bitmap、 DrawingCache 、背景、監(jiān)聽器等等都無法釋放,當(dāng)我們退出界面時,遞歸遍歷所有的子 view,釋放相關(guān)的資源,降低內(nèi)存泄露時所占用的內(nèi)存

收斂線程,祖?zhèn)鞔a在項目中有很多地方使用了 new Thread 、 AsyncTask 、自己創(chuàng)建線程池等等操作,通過統(tǒng)一的線程池等手段減少 App 創(chuàng)建線程數(shù)量,降低系統(tǒng)的開銷

針對低端機和高端機采用不同的策略,減少低端機內(nèi)存的占用

內(nèi)存泄露是永遠(yuǎn)也解決不完的,所以需要梳理一下 Top 系列泄露問題,重點解決占用內(nèi)存最多的泄露,以及使用頻率最高的場景所產(chǎn)生的泄露

繁創(chuàng)建小對象,堆內(nèi)存累計過大,這些一般都是有明顯堆棧的,根據(jù)堆棧信息解決即可。例如在循環(huán)動畫中一直創(chuàng)建 Bitmap

大對象,堆的單次分配內(nèi)存過大

刪減代碼,減少 dex 文件占用的內(nèi)存

減少 App 中 dex 數(shù)量,非必要功能,可以通過動態(tài)下發(fā)

按需加載 so 文件,不要提前加載所有的 so 文件,需要使用時再去加載

Java 堆上還有很多可用的內(nèi)存,為什么還會出現(xiàn) OOM

很多小伙伴們都問過我這么一個問題,大概歸因了一下,主要有以下幾個原因:

內(nèi)存碎片化,沒有足夠的連續(xù)段的內(nèi)存分配

虛擬內(nèi)存不足

線程或者 FD 的數(shù)量超過當(dāng)前手機的閾值

文章的最后想提一點,我們在做性能優(yōu)化的時候,不僅要關(guān)心性能指標(biāo)數(shù)據(jù),還需要關(guān)心對業(yè)務(wù)指標(biāo)數(shù)據(jù)的影響,比如對使用時長、留存等等能提升多少。

為什么需要關(guān)心業(yè)務(wù)指標(biāo)數(shù)據(jù)?

性能指標(biāo)數(shù)據(jù),比如 OOM 崩潰率、Native 崩潰率、ANR 等等、可能只有客戶端的小伙伴才知道 OOM、Native、ANR 是什么意思,但是其他人(產(chǎn)品經(jīng)理、老板等等)他們是不知道的,也不會去關(guān)心這些,但是他們對使用時長、留存等業(yè)務(wù)指標(biāo)數(shù)據(jù)更加的敏感,更能夠體現(xiàn)做這件事的價值,這只是闡述了我自己的觀點,每個人站的角度不一樣,觀點也不一樣。

全文到這里就結(jié)束了,這篇文章只是梳理一下內(nèi)存相關(guān)的知識點,以及有那些因素會導(dǎo)致

崩潰和相對應(yīng)的解決方案。下篇文章將會介紹堆內(nèi)存,堆內(nèi)存是程序在運行過程中為對象分配內(nèi)存的區(qū)域。

廣告聲明:文內(nèi)含有的對外跳轉(zhuǎn)鏈接(包括不限于超鏈接、二維碼、口令等形式),用于傳遞更多信息,節(jié)省甄選時間,結(jié)果僅供參考,IT之家所有文章均包含本聲明。

相關(guān)文章

關(guān)鍵詞:內(nèi)存,OOM

軟媒旗下網(wǎng)站: IT之家 最會買 - 返利返現(xiàn)優(yōu)惠券 iPhone之家 Win7之家 Win10之家 Win11之家

軟媒旗下軟件: 軟媒手機APP應(yīng)用 魔方 最會買 要知