對(duì)于在開發(fā)中內(nèi)存的管理是尤為重要的,尤其是在軟件的設(shè)計(jì)中,今天我們就來講講有關(guān)于:“android要怎么進(jìn)行內(nèi)存管理?”這個(gè)問題的解決方法和相關(guān)的解決思路分享!
前言
很高興遇見你~
內(nèi)存優(yōu)化一直是 Android 開發(fā)中的一個(gè)非常重要的話題,他直接影響著我們 app 的性能表現(xiàn)。但這個(gè)話題涉及到的內(nèi)容很廣且都偏向底層,讓很多開發(fā)者望而卻步。同時(shí),內(nèi)存優(yōu)化更加偏向于“經(jīng)驗(yàn)知識(shí)”,需要在實(shí)際項(xiàng)目中去應(yīng)用來學(xué)習(xí)。
因而本文并不想深入到底層去講內(nèi)存優(yōu)化的原理,而是著眼于宏觀,聊聊 android 是如何分配和管理內(nèi)存、在內(nèi)存不足的時(shí)候系統(tǒng)會(huì)如何處理以及會(huì)對(duì)用戶造成什么樣的影響。
Android 應(yīng)用基于 JVM 語言進(jìn)行開發(fā),雖然 google 根據(jù)移動(dòng)設(shè)備特點(diǎn)開發(fā)了自家的虛擬機(jī)如 Dalvik、ART,但依舊是基于 JVM 模型,在堆區(qū)分配對(duì)象內(nèi)存。因此 Java heap(java 堆)是android應(yīng)用內(nèi)存分配和回收的重點(diǎn)。其次,移動(dòng)設(shè)備的 RAM 非常有限,如何為進(jìn)程分配以及管理內(nèi)存也是重中之重。
文章的主要內(nèi)容是分析 Java heap、RAM 的內(nèi)存管理,以及當(dāng)內(nèi)存不夠時(shí) android 會(huì)如何處理。
那么,我們開始吧。
Java Heap
Java Heap,也就是 JVM 中的堆區(qū)。簡單回顧一下 JVM 中運(yùn)行時(shí)數(shù)據(jù)區(qū)域的劃分:
- 橙色區(qū)域的方法棧以及程序計(jì)數(shù)器屬于線程私有,主要存儲(chǔ)方法中的局部數(shù)據(jù)。
- 方法區(qū)主要存儲(chǔ)常量以及類信息,線程共享。
- 堆區(qū)主要負(fù)責(zé)存儲(chǔ)創(chuàng)建的對(duì)象,幾乎一切對(duì)象的內(nèi)存都在堆區(qū)中分配,同時(shí)也是線程共享。
我們?cè)?android 程序中使用如 Object o = new Object() 代碼創(chuàng)建的對(duì)象都會(huì)在堆區(qū)中分配一塊內(nèi)存進(jìn)行存儲(chǔ),具體如何分配由虛擬機(jī)解決而不需要我們開發(fā)者干預(yù)。當(dāng)一個(gè)對(duì)象不再使用時(shí), JVM 中具有垃圾回收機(jī)制(GC),會(huì)自動(dòng)釋放堆區(qū)中無用的對(duì)象,重新利用內(nèi)存。當(dāng)我們請(qǐng)求分配的內(nèi)存已經(jīng)超過堆區(qū)的內(nèi)存大小,則會(huì)拋出 OOM 異常。
在 android 中,堆區(qū)是一個(gè)由 JVM 邏輯劃分的區(qū)域,他并不是真正的物理區(qū)域。堆區(qū)并不會(huì)直接全部映射和他等量大小的物理內(nèi)存,而是到了需要使用時(shí),才會(huì)去建立邏輯地址和物理地址的映射:
這樣可以給應(yīng)用分配足夠的邏輯內(nèi)存大小,同時(shí)也不必在啟動(dòng)時(shí)一次性分配一大塊的物理內(nèi)存。在相同大小的內(nèi)存中,可以運(yùn)行更多的程序。
當(dāng)堆區(qū)進(jìn)程 GC 之后,釋放出來多余的空閑內(nèi)存,會(huì)返還給系統(tǒng),減少物理內(nèi)存的占用。但這個(gè)過程涉及到比較復(fù)雜的系統(tǒng)調(diào)用,若釋放的內(nèi)存較為少量,可能得不償失,則無需返還給系統(tǒng),在堆區(qū)中繼續(xù)使用即可。
在 GC 過程中,如果一個(gè)對(duì)象不再使用,但是其所占用的內(nèi)存無法被釋放,導(dǎo)致資源浪費(fèi),這種現(xiàn)象稱為內(nèi)存泄漏。內(nèi)存泄露會(huì)導(dǎo)致堆區(qū)中的對(duì)象越來越多,內(nèi)存的壓力越來越大,甚至出現(xiàn) OOM 。因此,內(nèi)存泄露是我們必須要盡量避免的現(xiàn)象。
進(jìn)程內(nèi)存分配
堆區(qū)的內(nèi)存分配,屬于進(jìn)程內(nèi)的內(nèi)存分配,由進(jìn)程自己管理。下面講一個(gè)應(yīng)用,系統(tǒng)是如何為其分配內(nèi)存的。
系統(tǒng)的運(yùn)行內(nèi)存,即為我們常說的 RAM ,是應(yīng)用的運(yùn)行空間。每個(gè)應(yīng)用必須裝入內(nèi)存中才可以被執(zhí)行:
- 我們安裝的應(yīng)用進(jìn)程都位于硬盤中
- 當(dāng)一個(gè)應(yīng)用被執(zhí)行時(shí),需要裝入到 RAM 中才能被執(zhí)行(zRAM 是為了壓縮數(shù)據(jù)節(jié)省空間而設(shè)計(jì),后續(xù)會(huì)講到)
- CPU 與 RAM 交互,讀取指令、數(shù)據(jù)、寫入數(shù)據(jù)等
RAM 的大小為設(shè)備的硬件內(nèi)存大小,是非常寶貴的資源?,F(xiàn)代手機(jī)常見的運(yùn)存是6G、8G或者12G,一些專為游戲研發(fā)的手機(jī)甚至有18G,但同時(shí)價(jià)格也會(huì)跟上去。
Android 采用分頁存儲(chǔ)的方式把一個(gè)進(jìn)程存儲(chǔ)到 RAM 中。分頁存儲(chǔ),簡單來說就是把內(nèi)存分割成很多個(gè)小塊,每個(gè)應(yīng)用占用不同的小塊,這些小塊也可以稱為頁:
前面講到,進(jìn)程的堆區(qū)并不是一次性分配,當(dāng)需要分配內(nèi)存時(shí),系統(tǒng)會(huì)為其分配空閑的頁;當(dāng)這些頁被回收,那么有可能被返還到系統(tǒng)中。
這里的頁、塊概念涉及到操作系統(tǒng)的分頁存儲(chǔ),這里并不打算展開詳細(xì)講解,有興趣的讀者可以自行了解:分頁存儲(chǔ)-維基百科。本文中的“頁”與“塊”可以不嚴(yán)謹(jǐn)?shù)乩斫鉃橥瑐€(gè)概念,為了幫助理解這里不進(jìn)行詳細(xì)地區(qū)分。
分配給進(jìn)程的頁可以分為兩種類型:干凈頁、臟頁:
- 干凈頁:進(jìn)程從硬盤中讀取數(shù)據(jù)或申請(qǐng)內(nèi)存之后未進(jìn)行修改。這種類型的頁面在內(nèi)存不足的時(shí)候可以被回收,因?yàn)轫撝写鎯?chǔ)的數(shù)據(jù)可通過其他的途徑復(fù)原。
- 臟頁:進(jìn)程對(duì)頁中的數(shù)據(jù)進(jìn)行了修改或數(shù)據(jù)存儲(chǔ)。這類頁面不能被直接回收,否則會(huì)造成數(shù)據(jù)丟失,必須先進(jìn)行數(shù)據(jù)存儲(chǔ)。
zRAM,是作為 RAM 中的一個(gè)分區(qū),當(dāng)內(nèi)存不足時(shí),可以把一些類型的頁壓縮之后存儲(chǔ)在zRAM中,當(dāng)需要使用的時(shí)候再從zRAM中調(diào)出。通過壓縮來節(jié)省應(yīng)用的空間占用,同時(shí)不需要與硬盤進(jìn)行調(diào)度,提高了速度。
這里需要理解的一個(gè)點(diǎn)是:內(nèi)存中的操作速度要遠(yuǎn)遠(yuǎn)比硬盤操作快。即使與zRAM的調(diào)入和調(diào)出需要壓縮和解壓,其速度也是比與硬盤交互快得多。
內(nèi)存不足管理
前面我們一直強(qiáng)調(diào),移動(dòng)設(shè)備的內(nèi)存容量是非常有限的,需要我們非常謹(jǐn)慎地去使用它。幸運(yùn)的是,JVM 和 android 系統(tǒng)早就幫我們想到了這一點(diǎn)。
面對(duì)不同的內(nèi)存壓力,android 會(huì)有不同的應(yīng)對(duì)策略。從低到高依次是 GC、內(nèi)核交換守護(hù)進(jìn)程釋放內(nèi)存、低內(nèi)存終止守護(hù)進(jìn)程殺死進(jìn)程釋放內(nèi)存;他們的代價(jià)也是逐步上升。下面我們依個(gè)來介紹一下。
GC 垃圾回收
GC 屬于 JVM 內(nèi)部的內(nèi)存管理機(jī)制,他管理的內(nèi)存區(qū)域是堆區(qū)。當(dāng)我們創(chuàng)建的對(duì)象越來多,堆區(qū)的壓力越來越大時(shí),GC 機(jī)制就會(huì)啟動(dòng),開始回收堆區(qū)中的垃圾對(duì)象。
辨別一個(gè)對(duì)象是否是垃圾,虛擬機(jī)采用的是可達(dá)性分析法。即從一些確定活躍有用的對(duì)象出發(fā),向下分析他的引用鏈;如果一個(gè)對(duì)象直接或者間接這些對(duì)象所引用,那么他就不是垃圾,否則就是垃圾。這些確定活躍有用的對(duì)象稱為 GC Roots:
如上圖,其中綠色的對(duì)象被 GC Roots 直接或間接引用,則不會(huì)被回收;灰色的對(duì)象沒有被引用則被標(biāo)記為垃圾
GC Roots對(duì)象的類型比較常見的是靜態(tài)變量以及棧中的引用。靜態(tài)變量比較好理解,他在整個(gè)進(jìn)程的執(zhí)行期間不會(huì)被回收,因此他肯定是有用的。棧,這里指的是 JVM 運(yùn)行數(shù)據(jù)區(qū)域中的方法棧,也就是局部變量引用,在方法執(zhí)行期間肯定是活躍的。由于方法棧屬于線程私有,因此這里等于活躍線程持有的對(duì)象不會(huì)被回收。
因此,如果一個(gè)對(duì)象對(duì)于我們的程序不再使用,則必須解除 GC Roots 對(duì)其的引用,否則會(huì)造成內(nèi)存泄露。例如,不要把 activity 賦值給一個(gè)靜態(tài)變量,這樣會(huì)導(dǎo)致界面退出時(shí)activity無法被回收。
GC 也并不是直接對(duì)整個(gè)堆區(qū)進(jìn)行回收,而是將堆區(qū)中的對(duì)象分成兩個(gè)部分:新生代、老年代。
剛創(chuàng)建的對(duì)象大都會(huì)被回收,而在多次回收中存活的對(duì)象則后續(xù)也很少被回收。新生代中存儲(chǔ)的對(duì)象主要是剛被創(chuàng)建不久的對(duì)象,而老年代則存儲(chǔ)著那些在多次 GC 中存活的對(duì)象。那么我們可以針對(duì)這些不同特性的對(duì)象,執(zhí)行不同的回收算法來提高GC性能:
- 對(duì)于新創(chuàng)建的對(duì)象,我們需要更加頻繁地對(duì)他們進(jìn)行GC來釋放內(nèi)存,且每次只需要記錄需要留下來的對(duì)象即可,而不必要去標(biāo)記其他大量需要被回收的對(duì)象,提高性能。
- 對(duì)于熬過很多次GC的對(duì)象,則可以以更低的頻率對(duì)他門進(jìn)行GC,且每次只需要關(guān)注少量需要被回收的對(duì)象即可。
具體的垃圾回收算法就不繼續(xù)展開了,了解到這里就可以。感興趣的讀者可以閱讀相關(guān)書籍。
單次的垃圾回收速度是很快的,甚至我們都無法感知到。但當(dāng)內(nèi)存壓力越來越大,垃圾回收的速度跟不上內(nèi)存分配的速度,此時(shí)就會(huì)出現(xiàn)內(nèi)存分配等待 GC 的情況,也就是發(fā)生了卡頓。同時(shí),我們無法控制 GC 的時(shí)機(jī),JVM 有一套完整的算法來決定什么時(shí)候進(jìn)行 GC。假如在我們滑動(dòng)界面的時(shí)候觸發(fā) GC ,那么展示出來的就是出現(xiàn)了掉幀情況。因此,做好內(nèi)存優(yōu)化,對(duì)于 app 的性能表現(xiàn)非常重要。
內(nèi)核交換守護(hù)進(jìn)程
GC 是針對(duì)于 Java 程序內(nèi)部進(jìn)行的優(yōu)化。對(duì)于移動(dòng)設(shè)備來說,RAM 非常寶貴,如何在有限的 RAM 資源上進(jìn)行分配內(nèi)存,也是一個(gè)非常重要的話題。
我們的應(yīng)用程序都運(yùn)行在 RAM 中,當(dāng)進(jìn)程不斷申請(qǐng)內(nèi)存分配,RAM 的剩余內(nèi)存達(dá)到一定的閾值時(shí),會(huì)啟動(dòng)內(nèi)核交換守護(hù)進(jìn)程來釋放內(nèi)存以滿足資源的分配。
內(nèi)核交換守護(hù)進(jìn)程,是運(yùn)行在系統(tǒng)內(nèi)核的一個(gè)進(jìn)程,他主要的工作時(shí)回收干凈頁、壓縮頁等操作來釋放內(nèi)存。前面講到,android 是基于分頁存儲(chǔ)的操作系統(tǒng),每個(gè)進(jìn)程都會(huì)被存儲(chǔ)到一些頁中。分頁的類型有兩種:干凈頁、臟頁:
- 當(dāng)內(nèi)核交換守護(hù)進(jìn)程啟動(dòng)時(shí),他會(huì)把干凈頁回收以釋放內(nèi)存。當(dāng)進(jìn)程再次訪問干凈頁時(shí),則需要去硬盤中再次讀取。
- 對(duì)于臟頁,內(nèi)核交換守護(hù)進(jìn)程會(huì)把他們壓縮后放入 zRAM 中。當(dāng)進(jìn)程訪問臟頁時(shí),則需要從zRAM中解壓出來。
通過不斷回收和壓縮分頁的方式來釋放內(nèi)存,以滿足新的內(nèi)存請(qǐng)求。使用此方式釋放的內(nèi)存也無法滿足新的內(nèi)存請(qǐng)求時(shí),android 會(huì)啟動(dòng)低內(nèi)存終止守護(hù)進(jìn)程,來終止一些低優(yōu)先級(jí)的進(jìn)程。
低內(nèi)存終止守護(hù)進(jìn)程
當(dāng) RAM 的被占用內(nèi)存達(dá)到一定的閾值,android 會(huì)根據(jù)進(jìn)程的優(yōu)先級(jí),終止部分進(jìn)程來釋放內(nèi)存。當(dāng)?shù)蛢?nèi)存終止守護(hù)進(jìn)程啟動(dòng)時(shí),說明系統(tǒng)的內(nèi)存壓力已經(jīng)非常大了,這在一些性能較差的設(shè)備中經(jīng)常出現(xiàn)。
進(jìn)程的優(yōu)先級(jí)從高到低排序如下,優(yōu)先級(jí)更高的進(jìn)程會(huì)優(yōu)先被終止:
圖片來源:developer.android.google.cn/topic/perfo…
從上到下依次是:
- 后臺(tái)應(yīng)用:使用過的 app 會(huì)被緩存在后臺(tái),下一次打開可以更加快速地進(jìn)行切換。當(dāng)內(nèi)存不足時(shí),此類應(yīng)用會(huì)最快被殺死。
- 上一個(gè)應(yīng)用:例如從微信跳轉(zhuǎn)到瀏覽器,此時(shí)微信就是上一個(gè)應(yīng)用。
- 主屏幕應(yīng)用:這是啟動(dòng)器應(yīng)用,也就是我們的桌面。如果這個(gè)進(jìn)程被kill了,那么返回桌面時(shí)會(huì)暫時(shí)黑屏。
- 服務(wù):同步服務(wù)、上傳服務(wù)等等
- 可覺察的應(yīng)用:例如正在播放的音樂軟件,他可以被我們感知到,但是不在前臺(tái)。
- 前臺(tái)應(yīng)用:當(dāng)前正在使用的應(yīng)用,如果這個(gè)應(yīng)用被kill了,需要向用戶報(bào)崩潰異常,此時(shí)的體驗(yàn)是極差的。
- 持久性(服務(wù)):這些是設(shè)備的核心服務(wù),例如電話和 WLAN。
- 系統(tǒng):系統(tǒng)進(jìn)程。這些進(jìn)程被終止后,手機(jī)可能即將重新啟動(dòng),就像手機(jī)突然卡死重啟。
- 原生:系統(tǒng)使用的極低級(jí)別的進(jìn)程,例如我們的內(nèi)核交換守護(hù)進(jìn)程。
當(dāng)內(nèi)存不足,會(huì)按照上面的規(guī)則,從上到下來終止進(jìn)程,獲得內(nèi)存資源。這也就是為什么在 android 中我們的后臺(tái)應(yīng)用一直被殺死。為了避免我們的應(yīng)用被優(yōu)化,內(nèi)存優(yōu)化就顯得非常重要了。
最后再來回顧一下:
- 在0-1階段,系統(tǒng)的內(nèi)存資源足夠,程序請(qǐng)求內(nèi)存分配,系統(tǒng)會(huì)不斷地使用空閑頁來滿足應(yīng)用的內(nèi)存請(qǐng)求
- 在1-2階段,系統(tǒng)的可利用內(nèi)存下降到一個(gè)閾值,程序繼續(xù)請(qǐng)求內(nèi)存分配,內(nèi)核交換守護(hù)進(jìn)程啟動(dòng),開始釋放緩存來滿足內(nèi)存請(qǐng)求
- 在2-3階段,系統(tǒng)的被利用內(nèi)存達(dá)到一個(gè)閾值,系統(tǒng)將啟動(dòng)低內(nèi)存終止守護(hù)進(jìn)程來殺死進(jìn)程釋放內(nèi)存
最后
我們文章分析了 android 是如何對(duì)內(nèi)存進(jìn)行分配以及低內(nèi)存時(shí)如何釋放內(nèi)存來滿足內(nèi)存請(qǐng)求??梢院苊黠@看到,當(dāng)內(nèi)存不足時(shí),會(huì)嚴(yán)重影響我們 app 的體驗(yàn)甚至整個(gè)用戶手機(jī)的體驗(yàn):
- 當(dāng)內(nèi)存不足會(huì)造成頻繁GC、回收干凈頁、回寫緩存,導(dǎo)致應(yīng)用緩慢、卡頓
- 如果設(shè)備內(nèi)存一直不夠,那么會(huì)一直殺死進(jìn)程影響用戶體驗(yàn),特別是這些進(jìn)程是用戶非常在意的如游戲、微信
- 內(nèi)存占用過高會(huì)讓app在后臺(tái)被殺死、或者讓用戶的其他app被殺死、甚至整個(gè)系統(tǒng)無法運(yùn)行而直接崩潰重啟,
- 不是所有的設(shè)備都有著高內(nèi)存,有著設(shè)備只有很少的內(nèi)存,在一些性能較差的設(shè)備上甚至?xí)o法運(yùn)行,這樣我們就失去了這些設(shè)備的市場
反觀現(xiàn)在國內(nèi)的很多 app,有如扣扣、t寶、iqy,在我這個(gè)三年前的機(jī)器上運(yùn)行會(huì)發(fā)生嚴(yán)重卡頓,偶爾還有ANR崩潰的出現(xiàn);而當(dāng)我去測(cè)試了youto、tele、Twit等 app ,發(fā)現(xiàn)基本不會(huì)發(fā)生卡頓,甚至在 youto 這樣有大量圖片視頻加載的 app 界面切換也盡享絲滑。這兩種 app 的體驗(yàn)是有著天壤之別的。
本文沒有講如何進(jìn)行內(nèi)存優(yōu)化,是因?yàn)檫@一塊的內(nèi)容設(shè)計(jì)到的太廣太深,無法在這篇文章中一并介紹。文章的目的只是為了幫助讀者了解android是如何管理內(nèi)存以及內(nèi)存不足可能造成的后果,對(duì)內(nèi)存的重要性能有一個(gè)感性的認(rèn)知。
在文中小編詳細(xì)的講解了有關(guān)于“android要怎么進(jìn)行內(nèi)存管理?”這個(gè)問題的解決方法,更多有關(guān)于android這方面的相關(guān)內(nèi)容我們都可以在W3Cschool進(jìn)行學(xué)習(xí)和了解!