最近一直在做內(nèi)存和 ANR 相關(guān)的優(yōu)化饵较,接下來(lái)我將會(huì)花幾篇文章梳理一下內(nèi)存相關(guān)的優(yōu)化竹观,以及我是如何將 OOM 崩潰率下降 90%镐捧。
今天這篇文章主要介紹內(nèi)存相關(guān)的知識(shí)點(diǎn),以及那些因素會(huì)導(dǎo)致 OOM 崩潰和相對(duì)應(yīng)的解決方案臭增,所以通過這篇文章你將學(xué)習(xí)到以下內(nèi)容:
- 什么是虛擬內(nèi)存和物理內(nèi)存
- 32 位和 64 位設(shè)備可用虛擬內(nèi)存分別是多少
- 為什么虛擬內(nèi)存不足主要發(fā)生在 32 位的設(shè)備上
- 如何解決虛擬內(nèi)存不足的問題
- App 啟動(dòng)完成之后懂酱,虛擬內(nèi)存的分布
- 如何解決 Java 堆內(nèi)存不足的問題
- Java 堆上還有很多可用的內(nèi)存,為什么還會(huì)出現(xiàn) OOM
- 做性能優(yōu)化時(shí)誊抛,需要關(guān)心那些指標(biāo)數(shù)據(jù)
不知道小伙伴們有沒有經(jīng)歷過列牺,相同的優(yōu)化方案,A 應(yīng)用上線之后拗窃,崩潰率下降很多瞎领,但是 B 應(yīng)用上線只有一點(diǎn)點(diǎn)收益,每個(gè)優(yōu)化方案随夸,在不同的 App 上所得到的優(yōu)化效果未必一樣九默,因?yàn)槊總€(gè) App 在不同的國(guó)家和地區(qū)面對(duì)的用戶群體不一樣,因此機(jī)型也都不一樣宾毒,所以我們需要了解內(nèi)存相關(guān)的知識(shí)點(diǎn)驼修,結(jié)合線上和線下數(shù)據(jù),對(duì)自己的 App 進(jìn)行歸因诈铛,對(duì)癥下藥乙各,才能取得較大的收益。
內(nèi)存是極其稀缺的資源幢竹,不合理的使用會(huì)導(dǎo)致可用內(nèi)存越來(lái)越少觅丰,可能會(huì)引發(fā)卡頓、ANR妨退、OOM 崩潰妇萄、Native 崩潰等等,嚴(yán)重影響用戶的體驗(yàn)咬荷。所以當(dāng)我們?cè)谧鲂阅軆?yōu)化的時(shí)候冠句,內(nèi)存優(yōu)化是非常重要的環(huán)節(jié)。
初期在做內(nèi)存優(yōu)化的時(shí)候幸乒,在我們的腦海里都會(huì)有一個(gè)潛意識(shí)「內(nèi)存占用越少越好」懦底,在某些情況下是不對(duì)的。例如在高端機(jī)上我們可以多分配點(diǎn)內(nèi)存罕扎,可以提升用戶的體驗(yàn)聚唐,但是在低端機(jī)上內(nèi)存本身就很小丐重,所以我們應(yīng)盡量減少內(nèi)存的分配。例如針對(duì)損耗性能的動(dòng)畫杆查、特效等等扮惦,在低端機(jī)上是不是可以關(guān)掉,或者關(guān)掉硬件加速亲桦、采用其他的方案代替崖蜜,這樣不僅可以減少崩潰,還可以減少卡頓客峭,提高用戶體驗(yàn)豫领。
因?yàn)?Java 有自動(dòng)回收機(jī)制,所以在開發(fā)過程中舔琅,很少有人會(huì)去關(guān)心內(nèi)存問題等恐,在腦海中都會(huì)有一個(gè)潛意識(shí) GC 會(huì)自動(dòng)回收,所以用完不會(huì)主動(dòng)釋放掉無(wú)用資源例如 Bitmap备蚓、動(dòng)畫课蔬、播放器等等,等待 GC 來(lái)回收星著,在實(shí)際項(xiàng)目中,依賴 GC 是不可靠的粗悯。首先 GC 自動(dòng)回收機(jī)制具有不確定性虚循,GC 也分為了不同的類型,如果發(fā)生 Full GC 時(shí)样傍,會(huì)觸發(fā) stop the work 事件横缔,會(huì)使 App 變得更加嚴(yán)重。
另外 GC 的回收機(jī)制根據(jù)可達(dá)性分析算法判斷一個(gè)對(duì)象是否可以被回收衫哥,如果存在內(nèi)存泄露茎刚,GC 是不會(huì)回收這些資源的,逐漸累積撤逢,當(dāng)達(dá)到堆的內(nèi)存上限時(shí)膛锭,發(fā)生 OOM 崩潰了,所以你要保證自己不要寫出內(nèi)存泄露的代碼蚊荣,以及團(tuán)隊(duì)其他人不要寫出內(nèi)存泄露的代碼初狰,然而實(shí)際情況這是不可能的,所以依靠 GC 自動(dòng)回收機(jī)制這種想法是不可靠的互例。雖然 Java 有內(nèi)存回收機(jī)制奢入,但是我們應(yīng)該在腦海中保留內(nèi)存管理的意識(shí),所以當(dāng)申請(qǐng)完內(nèi)存媳叨,退出或者不在使用時(shí)腥光,及時(shí)釋放掉內(nèi)存关顷。真正做到 用時(shí)分配,及時(shí)釋放武福。
可用內(nèi)存越來(lái)越少時(shí)议双,嚴(yán)重時(shí)會(huì)導(dǎo)致 OOM 崩潰,做過 OOM 優(yōu)化的朋友應(yīng)該會(huì)發(fā)現(xiàn)艘儒,線上捕獲的大部分 OOM 崩潰堆棧聋伦,都是壓死駱駝的最后一根稻草,并不是問題的根本所在界睁,所以我們需要對(duì) OOM 崩潰進(jìn)行歸因觉增,找到占用內(nèi)存的大頭。降低整機(jī)已使用的內(nèi)存翻斟,從而降低 OOM 崩潰逾礁,因此我大概分為了以下幾個(gè)方面。
- 虛擬內(nèi)存和物理內(nèi)存
- 堆內(nèi)存
- Java 堆內(nèi)存溢出
- 分配的內(nèi)存到達(dá) Java 堆的上限
- 可用內(nèi)存很多访惜,因?yàn)閮?nèi)存碎片化嘹履,沒有足夠的連續(xù)段的空間分配
- 對(duì)象的單次分配或者多次分配累計(jì)過大,例如在循環(huán)動(dòng)畫中一直創(chuàng)建 Bitmap
- 內(nèi)存泄露
- 堆內(nèi)存泄露债热,指的是在程序運(yùn)行時(shí)砾嫉,給對(duì)象分配的內(nèi)存,當(dāng)程序退出或者退出界面時(shí)窒篱,分配的內(nèi)存沒有釋放或者因?yàn)槠渌驘o(wú)法釋放
- 資源泄露焕刮,比如 FD、socket墙杯、線程等等配并,這些在每個(gè)手機(jī)上都是有數(shù)量的限制,如果使用了不釋放高镐,就會(huì)因?yàn)橘Y源的耗盡而崩潰溉旋,我們?cè)诰€上就出現(xiàn)過 FD 的泄露,導(dǎo)致崩潰率漲了 3 倍
- Java 堆內(nèi)存溢出
- FD 的數(shù)量超出當(dāng)前手機(jī)的閾值
- 線程的數(shù)量超出當(dāng)前手機(jī)的閾值
其中 FD 和線程崩潰占比很低嫉髓,因此這不是我們前期優(yōu)化的重點(diǎn)观腊。這篇文章我們重點(diǎn)介紹 虛擬內(nèi)存和物理內(nèi)存,下篇文章將會(huì)介紹堆內(nèi)存算行, 堆內(nèi)存是程序在運(yùn)行過程中為對(duì)象分配內(nèi)存的區(qū)域恕沫,它也屬于虛擬內(nèi)存的范圍。
虛擬內(nèi)存和物理內(nèi)存
介紹虛擬內(nèi)存之前纱意,我們需要先介紹物理內(nèi)存婶溯,物理內(nèi)存就是實(shí)實(shí)在在的內(nèi)存(即內(nèi)存條),如果應(yīng)用直接對(duì)物理內(nèi)存操作,會(huì)存在很多問題:
- 安全問題迄委,應(yīng)用之間的內(nèi)存空間沒有隔離褐筛,會(huì)導(dǎo)致應(yīng)用 A 可以修改應(yīng)用 B 的內(nèi)存數(shù)據(jù),這是非常不安全的
- 內(nèi)存空間利用率低叙身,應(yīng)用對(duì)內(nèi)存的使用會(huì)出現(xiàn)內(nèi)存碎片化的問題渔扎,即使還有很多內(nèi)存可以用,但是沒有足夠的連續(xù)段的內(nèi)存分配信轿,而導(dǎo)致崩潰
- 效率低晃痴,多個(gè)應(yīng)用同時(shí)對(duì)物理內(nèi)存進(jìn)行讀取和寫入時(shí),使用效率會(huì)非常低
為了解決上面的問題财忽,我們需要為每個(gè)應(yīng)用分配 "中間內(nèi)存" 最終會(huì)映射到物理內(nèi)存上倘核,這就是接下來(lái)要說的虛擬內(nèi)存。
操作系統(tǒng)會(huì)為每個(gè)應(yīng)用分配一個(gè)獨(dú)立的虛擬內(nèi)存即彪,實(shí)現(xiàn)應(yīng)用間的內(nèi)存隔離紧唱,避免了應(yīng)用 A 修改應(yīng)用 B 的內(nèi)存數(shù)據(jù)的問題,虛擬內(nèi)存最終會(huì)映射到物理內(nèi)存上隶校,當(dāng)應(yīng)用申請(qǐng)內(nèi)存時(shí)漏益,得到的是虛擬內(nèi)存,只有真正執(zhí)行寫操作時(shí)深胳,才會(huì)分配到物理內(nèi)存绰疤,好處是應(yīng)用可以使用連續(xù)的地址空間來(lái)訪問不連續(xù)的物理內(nèi)存。
每個(gè)應(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)的虛擬地址的長(zhǎng)度配置為 CONFIG_ARM64_VA_BITS=39醉途,即 Android 的 64 位應(yīng)用可使用的地址空間大小為 2^39=512GB
。
當(dāng) 32 位應(yīng)用在 64 位的設(shè)備上運(yùn)行時(shí)砖茸,可使用 4GB 虛擬地址空間隘擎,而 64 位應(yīng)用可使用 512GB 的空間。因此在 64 位機(jī)器上不存在虛擬空間不足的問題凉夯。因此在 2019 年的時(shí)候 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)存分配情況。 android.googlesource/frameworks/…
- 系統(tǒng)資源預(yù)分配顷扩,包含了 Zygote 進(jìn)程初始化時(shí)拐邪,需要加載 Framework 層的代碼和資源。供 Fork 出來(lái)的子進(jìn)程可以直接使用隘截。 Framework 資源包含:Framework 層 Java 代碼扎阶、so、art 虛擬機(jī)婶芭、各種靜態(tài)資源字體东臀、文件等等
- 系統(tǒng)預(yù)分配區(qū)域中其中
[anon:libwebview reservation]
區(qū)域占用 130MB 內(nèi)存 - App 自身資源,包括 App 中的代碼犀农、資源惰赋、 App 直接或者間接開啟線程消耗的棧空間呵哨、 App 申請(qǐng)的內(nèi)存赁濒、內(nèi)存文件映射等內(nèi)容。
- Java 堆用于分配
Java / Kotlin
創(chuàng)建的對(duì)象孟害。由 GC 管理和回收拒炎,GC 回收時(shí)將From Space
里的對(duì)象復(fù)制到To Space
,這兩片區(qū)域分別為dalvik-main space
和dalvik-main space 1
, 這兩片區(qū)域的大小和我當(dāng)前測(cè)試機(jī) Java 堆大小一樣挨务,都是 512 MB击你,如下圖所示
根據(jù) Android 源碼中的解釋玉组,Java 堆的大小應(yīng)該是根據(jù) RAM Size
來(lái)設(shè)置的,這是一個(gè)經(jīng)驗(yàn)值丁侄,廠商是可以更改的球切,如果手機(jī) Root 之后,自己也可以改绒障,無(wú)論 RAM 多大吨凑,到目前為止 Java 堆的上限默認(rèn)都是 512MB, Google 源碼的設(shè)置如下如下圖所示户辱。
RAM (MB)-dalvik-heap. Mk | heapsize (MB) |
---|---|
phone-hdpi-dalvik-heap. Mk | 32 |
512-dalvik-heap. Mk | 128 |
1024-dalvik-heap. Mk | 256 |
2048-dalvik-heap. Mk | 512 |
4096-dalvik-heap. Mk | 512 |
無(wú)論 RAM 多大鸵钝,到目前為止堆的上限默認(rèn)都是 512MB |
- 內(nèi)存文件映射,mmap 是一種內(nèi)存映射文件的方法庐镐,我們的 APK恩商、Dex、so 等等都是通過 mmap 讀取的必逆,會(huì)導(dǎo)致虛擬內(nèi)存增大怠堪,mmap 占用的內(nèi)存跟讀寫有關(guān)系
經(jīng)過分析內(nèi)核、系統(tǒng)資源名眉、以及各 APP 的資源占用粟矿,最后留給我們使用的內(nèi)存并不是很多,所以我們要合理使用系統(tǒng)資源损拢,真正做到 "用時(shí)分配陌粹,及時(shí)釋放"。
如何解決虛擬內(nèi)存不足的問題
目前業(yè)界也有很多黑科技來(lái)釋放因系統(tǒng)占用的虛擬內(nèi)存不足的問題福压,這些黑科技可以參考微信分享的文章 快速緩解 32 位 Android 環(huán)境下虛擬內(nèi)存地址空間不足的“黑科技”掏秩,大概有以下幾個(gè)方面的優(yōu)化。
- Native 線程默認(rèn)的椌D罚空間大小為 1M 左右蒙幻,經(jīng)過測(cè)試大部分情況下線程內(nèi)執(zhí)行的邏輯并不需要這么大的空間,因此 Native 線程椀ㄍ玻空間減半邮破,可以減少
pthread_create
OOM 崩潰 - 系統(tǒng)預(yù)分配區(qū)域中其中
[anon:libwebview reservation]
區(qū)域占用 130MB 內(nèi)存,可以嘗試釋放 WebView 預(yù)分配的內(nèi)存腐泻,減少一部分虛擬內(nèi)存 - 虛擬機(jī)堆空間減半决乎,在上面提到過有兩片大小相同的區(qū)域分別
dalvik-main space
和dalvik-main space 1
队询,虛擬機(jī)堆空間減半其實(shí)就是減少其中一個(gè)main space
所占用的內(nèi)存 - 快手針對(duì)垃圾回收器
jemalloc
的優(yōu)化派桩,釋放的是anon:libc_malloc
所占用的虛擬內(nèi)存 快手 Android 內(nèi)存分配器優(yōu)化探索 (一)
以下統(tǒng)計(jì)的是在 Android 7.0 App 首次啟動(dòng)完成 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 啟動(dòng)完成之后铆惑,虛擬內(nèi)存的分布
下圖是 App 在 Android 7.0 上啟動(dòng)完成之后所占用的虛擬內(nèi)存 (Vss),不同系統(tǒng)、不同的 App 虛擬內(nèi)存的分布都不一樣员魏,丑蛤,我們可以通過解析 /process/pid/smaps
文件,查看自己的 App 虛擬內(nèi)存分配情況撕阎。 android.googlesource/frameworks/…
正如上圖所示受裹,主要分為三個(gè)部分:
- dalvik(即 Java 堆),程序在運(yùn)行過程中為對(duì)象分配內(nèi)存的區(qū)域
- 程序文件
dex
虏束、so
棉饶、oat
- Native
針對(duì)上面的問題,我們?cè)陧?xiàng)目中通過以下手段進(jìn)行優(yōu)化镇匀,重點(diǎn)優(yōu)化 dalvik 占用的內(nèi)存照藻,因篇幅問題,將會(huì)在后面的文章中汗侵,做詳細(xì)的分析:
- Android 3.0 ~ Android 7.0 上主要將 Bitmap 對(duì)象和像素?cái)?shù)據(jù)統(tǒng)一放到 Java 堆中幸缕,Java 堆上限 512MB,而 Native 占用虛擬內(nèi)存晰韵,32 的設(shè)備可使用 3GB发乔,64 位的設(shè)備更大,因此我們可以嘗試將 Bitmap 分配到 Native 上雪猪,緩解 Java 堆的壓力列疗,降低 OOM 崩潰,方案可以參考 抖音 Android 性能優(yōu)化系列:Java OOM 優(yōu)化之 NativeBitmap 方案
- 使用第三方圖片庫(kù)時(shí)浪蹂,需要針對(duì)高端機(jī)和低端機(jī)設(shè)置圖片庫(kù)不同的緩存大小抵栈,這樣我們?cè)诟叨藱C(jī)上保證體驗(yàn)的同時(shí),降低低端機(jī) OOM 崩潰率
- 收斂 Bitmap坤次,避免重復(fù)創(chuàng)建 Bitmap古劲,退出界面及時(shí)釋放掉資源(Bitmap、動(dòng)畫缰猴、播放器等等資源)
- 內(nèi)存回收兜底策略产艾,當(dāng)
Activity
或者Fragment
泄露時(shí),與之相關(guān)聯(lián)的動(dòng)畫滑绒、Bitmap闷堡、DrawingCache
、背景疑故、監(jiān)聽器等等都無(wú)法釋放杠览,當(dāng)我們退出界面時(shí),遞歸遍歷所有的子 view纵势,釋放相關(guān)的資源踱阿,降低內(nèi)存泄露時(shí)所占用的內(nèi)存 - 收斂線程管钳,祖?zhèn)鞔a在項(xiàng)目中有很多地方使用了
new Thread
、AsyncTask
软舌、自己創(chuàng)建線程池等等操作才漆,通過統(tǒng)一的線程池等手段減少 App 創(chuàng)建線程數(shù)量,降低系統(tǒng)的開銷 - 針對(duì)低端機(jī)和高端機(jī)采用不同的策略佛点,減少低端機(jī)內(nèi)存的占用
- 內(nèi)存泄露是永遠(yuǎn)也解決不完的醇滥,所以需要梳理一下 Top 系列泄露問題,重點(diǎn)解決占用內(nèi)存最多的泄露超营,以及使用頻率最高的場(chǎng)景所產(chǎn)生的泄露
- 繁創(chuàng)建小對(duì)象腺办,堆內(nèi)存累計(jì)過大,這些一般都是有明顯堆棧的糟描,根據(jù)堆棧信息解決即可怀喉。例如在循環(huán)動(dòng)畫中一直創(chuàng)建 Bitmap
- 大對(duì)象,堆的單次分配內(nèi)存過大
- 刪減代碼船响,減少 dex 文件占用的內(nèi)存
- 減少 App 中 dex 數(shù)量躬拢,非必要功能,可以通過動(dòng)態(tài)下發(fā)
- 按需加載 so 文件见间,不要提前加載所有的 so 文件聊闯,需要使用時(shí)再去加載
Java 堆上還有很多可用的內(nèi)存,為什么還會(huì)出現(xiàn) OOM
很多小伙伴們都問過我這么一個(gè)問題米诉,大概歸因了一下菱蔬,主要有以下幾個(gè)原因:
- 內(nèi)存碎片化,沒有足夠的連續(xù)段的內(nèi)存分配
- 虛擬內(nèi)存不足
- 線程或者 FD 的數(shù)量超過當(dāng)前手機(jī)的閾值
文章的最后想提一點(diǎn)史侣,我們?cè)谧鲂阅軆?yōu)化的時(shí)候拴泌,不僅要關(guān)心性能指標(biāo)數(shù)據(jù),還需要關(guān)心對(duì)業(yè)務(wù)指標(biāo)數(shù)據(jù)的影響惊橱,比如對(duì)使用時(shí)長(zhǎng)蚪腐、留存等等能提升多少。
為什么需要關(guān)心業(yè)務(wù)指標(biāo)數(shù)據(jù)税朴?
性能指標(biāo)數(shù)據(jù)回季,比如 OOM 崩潰率、Native 崩潰率正林、ANR 等等泡一、可能只有客戶端的小伙伴才知道 OOM、Native觅廓、ANR 是什么意思鼻忠,但是其他人(產(chǎn)品經(jīng)理、老板等等)他們是不知道的哪亿,也不會(huì)去關(guān)心這些粥烁,但是他們對(duì)使用時(shí)長(zhǎng)、留存等業(yè)務(wù)指標(biāo)數(shù)據(jù)更加的敏感蝇棉,更能夠體現(xiàn)做這件事的價(jià)值讨阻,這只是闡述了我自己的觀點(diǎn),每個(gè)人站的角度不一樣篡殷,觀點(diǎn)也不一樣钝吮。
全文到這里就結(jié)束了,這篇文章只是梳理一下內(nèi)存相關(guān)的知識(shí)點(diǎn)板辽,以及有那些因素會(huì)導(dǎo)致 OOM 崩潰和相對(duì)應(yīng)的解決方案奇瘦。下篇文章將會(huì)介紹堆內(nèi)存, 堆內(nèi)存是程序在運(yùn)行過程中為對(duì)象分配內(nèi)存的區(qū)域劲弦。