App下載

android要怎么進(jìn)行內(nèi)存管理?詳解android管理內(nèi)存方法分享!

杯酒挽歌 2021-08-20 16:24:20 瀏覽數(shù) (2756)
反饋

對于在開發(fā)中內(nèi)存的管理是尤為重要的,尤其是在軟件的設(shè)計中,今天我們就來講講有關(guān)于:“android要怎么進(jìn)行內(nèi)存管理?”這個問題的解決方法和相關(guān)的解決思路分享! 

前言

很高興遇見你~

內(nèi)存優(yōu)化一直是 Android 開發(fā)中的一個非常重要的話題,他直接影響著我們 app 的性能表現(xiàn)。但這個話題涉及到的內(nèi)容很廣且都偏向底層,讓很多開發(fā)者望而卻步。同時,內(nèi)存優(yōu)化更加偏向于“經(jīng)驗(yàn)知識”,需要在實(shí)際項目中去應(yīng)用來學(xué)習(xí)。

因而本文并不想深入到底層去講內(nèi)存優(yōu)化的原理,而是著眼于宏觀,聊聊 android 是如何分配和管理內(nèi)存、在內(nèi)存不足的時候系統(tǒng)會如何處理以及會對用戶造成什么樣的影響。

Android 應(yīng)用基于 JVM 語言進(jìn)行開發(fā),雖然 google 根據(jù)移動設(shè)備特點(diǎn)開發(fā)了自家的虛擬機(jī)如 Dalvik、ART,但依舊是基于 JVM 模型,在堆區(qū)分配對象內(nèi)存。因此 Java heap(java 堆)是android應(yīng)用內(nèi)存分配和回收的重點(diǎn)。其次,移動設(shè)備的 RAM 非常有限,如何為進(jìn)程分配以及管理內(nèi)存也是重中之重。

文章的主要內(nèi)容是分析 Java heap、RAM 的內(nèi)存管理,以及當(dāng)內(nèi)存不夠時 android 會如何處理。

那么,我們開始吧。

Java Heap

Java Heap,也就是 JVM 中的堆區(qū)。簡單回顧一下 JVM 中運(yùn)行時數(shù)據(jù)區(qū)域的劃分:

  • 橙色區(qū)域的方法棧以及程序計數(shù)器屬于線程私有,主要存儲方法中的局部數(shù)據(jù)。
  • 方法區(qū)主要存儲常量以及類信息,線程共享。
  • 堆區(qū)主要負(fù)責(zé)存儲創(chuàng)建的對象,幾乎一切對象的內(nèi)存都在堆區(qū)中分配,同時也是線程共享。

我們在 android 程序中使用如 Object o = new Object() 代碼創(chuàng)建的對象都會在堆區(qū)中分配一塊內(nèi)存進(jìn)行存儲,具體如何分配由虛擬機(jī)解決而不需要我們開發(fā)者干預(yù)。當(dāng)一個對象不再使用時, JVM 中具有垃圾回收機(jī)制(GC),會自動釋放堆區(qū)中無用的對象,重新利用內(nèi)存。當(dāng)我們請求分配的內(nèi)存已經(jīng)超過堆區(qū)的內(nèi)存大小,則會拋出 OOM 異常。

在 android 中,堆區(qū)是一個由 JVM 邏輯劃分的區(qū)域,他并不是真正的物理區(qū)域。堆區(qū)并不會直接全部映射和他等量大小的物理內(nèi)存,而是到了需要使用時,才會去建立邏輯地址和物理地址的映射:

這樣可以給應(yīng)用分配足夠的邏輯內(nèi)存大小,同時也不必在啟動時一次性分配一大塊的物理內(nèi)存。在相同大小的內(nèi)存中,可以運(yùn)行更多的程序。

當(dāng)堆區(qū)進(jìn)程 GC 之后,釋放出來多余的空閑內(nèi)存,會返還給系統(tǒng),減少物理內(nèi)存的占用。但這個過程涉及到比較復(fù)雜的系統(tǒng)調(diào)用,若釋放的內(nèi)存較為少量,可能得不償失,則無需返還給系統(tǒng),在堆區(qū)中繼續(xù)使用即可。

在 GC 過程中,如果一個對象不再使用,但是其所占用的內(nèi)存無法被釋放,導(dǎo)致資源浪費(fèi),這種現(xiàn)象稱為內(nèi)存泄漏。內(nèi)存泄露會導(dǎo)致堆區(qū)中的對象越來越多,內(nèi)存的壓力越來越大,甚至出現(xiàn) OOM 。因此,內(nèi)存泄露是我們必須要盡量避免的現(xiàn)象。

進(jìn)程內(nèi)存分配

堆區(qū)的內(nèi)存分配,屬于進(jìn)程內(nèi)的內(nèi)存分配,由進(jìn)程自己管理。下面講一個應(yīng)用,系統(tǒng)是如何為其分配內(nèi)存的。

系統(tǒng)的運(yùn)行內(nèi)存,即為我們常說的 RAM ,是應(yīng)用的運(yùn)行空間。每個應(yīng)用必須裝入內(nèi)存中才可以被執(zhí)行:

  • 我們安裝的應(yīng)用進(jìn)程都位于硬盤中
  • 當(dāng)一個應(yīng)用被執(zhí)行時,需要裝入到 RAM 中才能被執(zhí)行(zRAM 是為了壓縮數(shù)據(jù)節(jié)省空間而設(shè)計,后續(xù)會講到)
  • CPU 與 RAM 交互,讀取指令、數(shù)據(jù)、寫入數(shù)據(jù)等

RAM 的大小為設(shè)備的硬件內(nèi)存大小,是非常寶貴的資源?,F(xiàn)代手機(jī)常見的運(yùn)存是6G、8G或者12G,一些專為游戲研發(fā)的手機(jī)甚至有18G,但同時價格也會跟上去。

Android 采用分頁存儲的方式把一個進(jìn)程存儲到 RAM 中。分頁存儲,簡單來說就是把內(nèi)存分割成很多個小塊,每個應(yīng)用占用不同的小塊,這些小塊也可以稱為頁:

前面講到,進(jìn)程的堆區(qū)并不是一次性分配,當(dāng)需要分配內(nèi)存時,系統(tǒng)會為其分配空閑的頁;當(dāng)這些頁被回收,那么有可能被返還到系統(tǒng)中。

這里的頁、塊概念涉及到操作系統(tǒng)的分頁存儲,這里并不打算展開詳細(xì)講解,有興趣的讀者可以自行了解:分頁存儲-維基百科。本文中的“頁”與“塊”可以不嚴(yán)謹(jǐn)?shù)乩斫鉃橥瑐€概念,為了幫助理解這里不進(jìn)行詳細(xì)地區(qū)分。

分配給進(jìn)程的頁可以分為兩種類型:干凈頁、臟頁:

  • 干凈頁:進(jìn)程從硬盤中讀取數(shù)據(jù)或申請內(nèi)存之后未進(jìn)行修改。這種類型的頁面在內(nèi)存不足的時候可以被回收,因?yàn)轫撝写鎯Φ臄?shù)據(jù)可通過其他的途徑復(fù)原。
  • 臟頁:進(jìn)程對頁中的數(shù)據(jù)進(jìn)行了修改或數(shù)據(jù)存儲。這類頁面不能被直接回收,否則會造成數(shù)據(jù)丟失,必須先進(jìn)行數(shù)據(jù)存儲。

zRAM,是作為 RAM 中的一個分區(qū),當(dāng)內(nèi)存不足時,可以把一些類型的頁壓縮之后存儲在zRAM中,當(dāng)需要使用的時候再從zRAM中調(diào)出。通過壓縮來節(jié)省應(yīng)用的空間占用,同時不需要與硬盤進(jìn)行調(diào)度,提高了速度。

這里需要理解的一個點(diǎn)是:內(nèi)存中的操作速度要遠(yuǎn)遠(yuǎn)比硬盤操作快。即使與zRAM的調(diào)入和調(diào)出需要壓縮和解壓,其速度也是比與硬盤交互快得多。

內(nèi)存不足管理

前面我們一直強(qiáng)調(diào),移動設(shè)備的內(nèi)存容量是非常有限的,需要我們非常謹(jǐn)慎地去使用它。幸運(yùn)的是,JVM 和 android 系統(tǒng)早就幫我們想到了這一點(diǎn)。

面對不同的內(nèi)存壓力,android 會有不同的應(yīng)對策略。從低到高依次是 GC、內(nèi)核交換守護(hù)進(jìn)程釋放內(nèi)存、低內(nèi)存終止守護(hù)進(jìn)程殺死進(jìn)程釋放內(nèi)存;他們的代價也是逐步上升。下面我們依個來介紹一下。

GC 垃圾回收

GC 屬于 JVM 內(nèi)部的內(nèi)存管理機(jī)制,他管理的內(nèi)存區(qū)域是堆區(qū)。當(dāng)我們創(chuàng)建的對象越來多,堆區(qū)的壓力越來越大時,GC 機(jī)制就會啟動,開始回收堆區(qū)中的垃圾對象。

辨別一個對象是否是垃圾,虛擬機(jī)采用的是可達(dá)性分析法。即從一些確定活躍有用的對象出發(fā),向下分析他的引用鏈;如果一個對象直接或者間接這些對象所引用,那么他就不是垃圾,否則就是垃圾。這些確定活躍有用的對象稱為 GC Roots:

如上圖,其中綠色的對象被 GC Roots 直接或間接引用,則不會被回收;灰色的對象沒有被引用則被標(biāo)記為垃圾
GC Roots對象的類型比較常見的是靜態(tài)變量以及棧中的引用。靜態(tài)變量比較好理解,他在整個進(jìn)程的執(zhí)行期間不會被回收,因此他肯定是有用的。棧,這里指的是 JVM 運(yùn)行數(shù)據(jù)區(qū)域中的方法棧,也就是局部變量引用,在方法執(zhí)行期間肯定是活躍的。由于方法棧屬于線程私有,因此這里等于活躍線程持有的對象不會被回收。

因此,如果一個對象對于我們的程序不再使用,則必須解除 GC Roots 對其的引用,否則會造成內(nèi)存泄露。例如,不要把 activity 賦值給一個靜態(tài)變量,這樣會導(dǎo)致界面退出時activity無法被回收。

GC 也并不是直接對整個堆區(qū)進(jìn)行回收,而是將堆區(qū)中的對象分成兩個部分:新生代、老年代。

剛創(chuàng)建的對象大都會被回收,而在多次回收中存活的對象則后續(xù)也很少被回收。新生代中存儲的對象主要是剛被創(chuàng)建不久的對象,而老年代則存儲著那些在多次 GC 中存活的對象。那么我們可以針對這些不同特性的對象,執(zhí)行不同的回收算法來提高GC性能:

  • 對于新創(chuàng)建的對象,我們需要更加頻繁地對他們進(jìn)行GC來釋放內(nèi)存,且每次只需要記錄需要留下來的對象即可,而不必要去標(biāo)記其他大量需要被回收的對象,提高性能。
  • 對于熬過很多次GC的對象,則可以以更低的頻率對他門進(jìn)行GC,且每次只需要關(guān)注少量需要被回收的對象即可。

具體的垃圾回收算法就不繼續(xù)展開了,了解到這里就可以。感興趣的讀者可以閱讀相關(guān)書籍。

單次的垃圾回收速度是很快的,甚至我們都無法感知到。但當(dāng)內(nèi)存壓力越來越大,垃圾回收的速度跟不上內(nèi)存分配的速度,此時就會出現(xiàn)內(nèi)存分配等待 GC 的情況,也就是發(fā)生了卡頓。同時,我們無法控制 GC 的時機(jī),JVM 有一套完整的算法來決定什么時候進(jìn)行 GC。假如在我們滑動界面的時候觸發(fā) GC ,那么展示出來的就是出現(xiàn)了掉幀情況。因此,做好內(nèi)存優(yōu)化,對于 app 的性能表現(xiàn)非常重要。

內(nèi)核交換守護(hù)進(jìn)程

GC 是針對于 Java 程序內(nèi)部進(jìn)行的優(yōu)化。對于移動設(shè)備來說,RAM 非常寶貴,如何在有限的 RAM 資源上進(jìn)行分配內(nèi)存,也是一個非常重要的話題。

我們的應(yīng)用程序都運(yùn)行在 RAM 中,當(dāng)進(jìn)程不斷申請內(nèi)存分配,RAM 的剩余內(nèi)存達(dá)到一定的閾值時,會啟動內(nèi)核交換守護(hù)進(jìn)程來釋放內(nèi)存以滿足資源的分配。

內(nèi)核交換守護(hù)進(jìn)程,是運(yùn)行在系統(tǒng)內(nèi)核的一個進(jìn)程,他主要的工作時回收干凈頁、壓縮頁等操作來釋放內(nèi)存。前面講到,android 是基于分頁存儲的操作系統(tǒng),每個進(jìn)程都會被存儲到一些頁中。分頁的類型有兩種:干凈頁、臟頁:

  • 當(dāng)內(nèi)核交換守護(hù)進(jìn)程啟動時,他會把干凈頁回收以釋放內(nèi)存。當(dāng)進(jìn)程再次訪問干凈頁時,則需要去硬盤中再次讀取。
  • 對于臟頁,內(nèi)核交換守護(hù)進(jìn)程會把他們壓縮后放入 zRAM 中。當(dāng)進(jìn)程訪問臟頁時,則需要從zRAM中解壓出來。

通過不斷回收和壓縮分頁的方式來釋放內(nèi)存,以滿足新的內(nèi)存請求。使用此方式釋放的內(nèi)存也無法滿足新的內(nèi)存請求時,android 會啟動低內(nèi)存終止守護(hù)進(jìn)程,來終止一些低優(yōu)先級的進(jìn)程。

低內(nèi)存終止守護(hù)進(jìn)程

當(dāng) RAM 的被占用內(nèi)存達(dá)到一定的閾值,android 會根據(jù)進(jìn)程的優(yōu)先級,終止部分進(jìn)程來釋放內(nèi)存。當(dāng)?shù)蛢?nèi)存終止守護(hù)進(jìn)程啟動時,說明系統(tǒng)的內(nèi)存壓力已經(jīng)非常大了,這在一些性能較差的設(shè)備中經(jīng)常出現(xiàn)。

進(jìn)程的優(yōu)先級從高到低排序如下,優(yōu)先級更高的進(jìn)程會優(yōu)先被終止:

圖片來源:developer.android.google.cn/topic/perfo…

從上到下依次是:

  • 后臺應(yīng)用:使用過的 app 會被緩存在后臺,下一次打開可以更加快速地進(jìn)行切換。當(dāng)內(nèi)存不足時,此類應(yīng)用會最快被殺死。
  • 上一個應(yīng)用:例如從微信跳轉(zhuǎn)到瀏覽器,此時微信就是上一個應(yīng)用。
  • 主屏幕應(yīng)用:這是啟動器應(yīng)用,也就是我們的桌面。如果這個進(jìn)程被kill了,那么返回桌面時會暫時黑屏。
  • 服務(wù):同步服務(wù)、上傳服務(wù)等等
  • 可覺察的應(yīng)用:例如正在播放的音樂軟件,他可以被我們感知到,但是不在前臺。
  • 前臺應(yīng)用:當(dāng)前正在使用的應(yīng)用,如果這個應(yīng)用被kill了,需要向用戶報崩潰異常,此時的體驗(yàn)是極差的。
  • 持久性(服務(wù)):這些是設(shè)備的核心服務(wù),例如電話和 WLAN。
  • 系統(tǒng):系統(tǒng)進(jìn)程。這些進(jìn)程被終止后,手機(jī)可能即將重新啟動,就像手機(jī)突然卡死重啟。
  • 原生:系統(tǒng)使用的極低級別的進(jìn)程,例如我們的內(nèi)核交換守護(hù)進(jìn)程。

當(dāng)內(nèi)存不足,會按照上面的規(guī)則,從上到下來終止進(jìn)程,獲得內(nèi)存資源。這也就是為什么在 android 中我們的后臺應(yīng)用一直被殺死。為了避免我們的應(yīng)用被優(yōu)化,內(nèi)存優(yōu)化就顯得非常重要了。

最后再來回顧一下:

圖片來源:www.youtube.com/watch?v=w7K

  • 在0-1階段,系統(tǒng)的內(nèi)存資源足夠,程序請求內(nèi)存分配,系統(tǒng)會不斷地使用空閑頁來滿足應(yīng)用的內(nèi)存請求
  • 在1-2階段,系統(tǒng)的可利用內(nèi)存下降到一個閾值,程序繼續(xù)請求內(nèi)存分配,內(nèi)核交換守護(hù)進(jìn)程啟動,開始釋放緩存來滿足內(nèi)存請求
  • 在2-3階段,系統(tǒng)的被利用內(nèi)存達(dá)到一個閾值,系統(tǒng)將啟動低內(nèi)存終止守護(hù)進(jìn)程來殺死進(jìn)程釋放內(nèi)存

最后

我們文章分析了 android 是如何對內(nèi)存進(jìn)行分配以及低內(nèi)存時如何釋放內(nèi)存來滿足內(nèi)存請求??梢院苊黠@看到,當(dāng)內(nèi)存不足時,會嚴(yán)重影響我們 app 的體驗(yàn)甚至整個用戶手機(jī)的體驗(yàn):

  • 當(dāng)內(nèi)存不足會造成頻繁GC、回收干凈頁、回寫緩存,導(dǎo)致應(yīng)用緩慢、卡頓
  • 如果設(shè)備內(nèi)存一直不夠,那么會一直殺死進(jìn)程影響用戶體驗(yàn),特別是這些進(jìn)程是用戶非常在意的如游戲、微信
  • 內(nèi)存占用過高會讓app在后臺被殺死、或者讓用戶的其他app被殺死、甚至整個系統(tǒng)無法運(yùn)行而直接崩潰重啟,
  • 不是所有的設(shè)備都有著高內(nèi)存,有著設(shè)備只有很少的內(nèi)存,在一些性能較差的設(shè)備上甚至?xí)o法運(yùn)行,這樣我們就失去了這些設(shè)備的市場

反觀現(xiàn)在國內(nèi)的很多 app,有如扣扣、t寶、iqy,在我這個三年前的機(jī)器上運(yùn)行會發(fā)生嚴(yán)重卡頓,偶爾還有ANR崩潰的出現(xiàn);而當(dāng)我去測試了youto、tele、Twit等 app ,發(fā)現(xiàn)基本不會發(fā)生卡頓,甚至在 youto 這樣有大量圖片視頻加載的 app 界面切換也盡享絲滑。這兩種 app 的體驗(yàn)是有著天壤之別的。

本文沒有講如何進(jìn)行內(nèi)存優(yōu)化,是因?yàn)檫@一塊的內(nèi)容設(shè)計到的太廣太深,無法在這篇文章中一并介紹。文章的目的只是為了幫助讀者了解android是如何管理內(nèi)存以及內(nèi)存不足可能造成的后果,對內(nèi)存的重要性能有一個感性的認(rèn)知。

在文中小編詳細(xì)的講解了有關(guān)于“android要怎么進(jìn)行內(nèi)存管理?”這個問題的解決方法,更多有關(guān)于android這方面的相關(guān)內(nèi)容我們都可以在W3Cschool進(jìn)行學(xué)習(xí)和了解! 

0 人點(diǎn)贊