Android性能優化(三)以內存管理

一、初識內存優化

在Android的性能優化的各個部分裏,內存的問題絕對是最使人頭疼的一部分,雖然Android有垃圾自動回收機制不須要手動干預,但也恰由於此,出現內存問題如內存泄漏和內存溢出等,若是對內存管理機制不熟悉,會更加難以排查問題。javascript

由於內存方面的知識較多且不易理解,內存優化部分就分兩篇文章進行,本文主要是關於Java、Android的內存分配、回收、GC等理論知識。html

二、內存分配

談Android的內存,就不能不提Java的內存管理。Java程序在運行的過程當中會將其管理的內存分爲若干個不一樣的數據區:java

JVM運行時數據區

方法區:方法區存放的是類信息、常量、靜態變量,全部線程共享區域。android

虛擬機棧:每一個方法在執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息,線程私有區域。算法

本地方法棧:與虛擬機棧相似,區別是虛擬機棧爲虛擬機執行Java方法服務,本地方法棧爲虛擬機使用到的Native方法服務shell

JVM管理的內存中最大的一塊,全部線程共享;用來存放對象實例,幾乎全部的對象實例都在堆上分配內存;此區域也是垃圾回收器(Garbage Collection)主要的做用區域,內存泄漏就發生在這個區域緩存

程序計數器可看作是當前線程所執行的字節碼的行號指示器;若是線程在執行Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令地址;若是執行的是Native方法,這個計數器的值爲空(Undefined)。性能優化

備註:
有一種習慣說法:把Java的內存區域分爲堆內存(Heap)和棧內存(Stack),Stack訪問快,Heap訪問慢,Stack中保存的是對象的引用(指針),Heap中保存的是對象的實例。微信

實際上這種說法是籠統、粗糙的,此處所說的Stack僅僅是虛擬機棧中的局部變量表部分。虛擬機棧與JVM運行時數據區涵蓋的都比此種說法多。併發

三、內存回收

3.1標記-清除算法

最基礎的收集算法:分爲「標記」和「清除」兩個階段,首先,標記出全部須要回收的對象,而後統一回收全部被標記的對象。
這種方法有兩個不足點:

  1. 效率問題,標記和清除兩個過程的效率都不高;
  2. 空間問題,標記清除以後會產生大量的不連續的內存碎片。

「標記-清除」算法示意圖

3.2複製算法

將內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊,當這一塊內存將用完了,就將還存活着的對象複製到另外一塊內存上面,而後再把已使用過的內存空間一次清理掉。
這種方法的特色:

  • 優勢:實現簡單,運行高效;每次都是對整個半區進行內存回收,內存分配時也不須要考慮內存碎片等狀況,只要移動堆頂指針,按順序分配內存便可;
  • 缺點:粗暴的將內存縮小爲原來的一半,代價實在有點高。

「複製」算法示意圖

3.3標記-整理算法

先標記須要回收的對象(標記過程與「標記-清除」算法同樣),而後把全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。
這種方法的特色:

  • 避免了內存碎片;
  • 避免了「複製」算法50%的空間浪費;
  • 主要針對對象存活率高的老年代。

「標記-整理」算法示例圖.png

3.4分代收集算法

根據對象的存活週期的不一樣將內存劃分爲幾塊,通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都會發現有大量對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清除算法或標記—整理算法來進行回收。

四、對象是否回收的依據

4.1引用計數算法

給對象中添加一個引用計數器,每當有一個地方引用該對象時,計數器值加1;引用失效時,計數器值減1;任意時刻計數器爲0的對象就是不可能再被使用的,表示該對象不存在引用關係。
這種方法的特色:

  • 優勢:實現簡單,斷定效率也很高;
  • 缺點:難以解決對象之間相互循環引用致使計數器值不等於0的問題。

4.2可達性分析算法

以一系列成爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連(GC Roots到這個對象不可達),則證實此對象是不可用的。

可達性分析算法斷定對象是否可回收

五、Android的內存管理

Android系統的ART和Dalvik虛擬機扮演了常規的內存垃圾自動回收的角色, 使用pagingmemory-mapping來管理內存,這意味着無論是由於建立對象仍是使用使用內存頁面形成的任何被修改的內存,都會一直存在於內存中,App惟一釋放內存的方法就是釋放App持有的對象引用,使GC能夠回收。

Android Runtime內存堆劃分

  • 5.1內存回收

    在Android的高級系統版本里面針對Heap空間有一個Generational Heap Memory的模型,最近分配的對象會存放在Young Generation區域,當這個對象在這個區域停留的時間達到必定程度,它會被移動到Old Generation,最後累積必定時間再移動到Permanent Generation區域。系統會根據內存中不一樣的內存數據類型分別執行不一樣的gc操做。例如,剛分配到Young Generation區域的對象一般更容易被銷燬回收,同時在Young Generation區域的gc操做速度會比Old Generation區域的gc操做速度更快。

  • 5.2共享內存

    1. Android應用的進程都是從一個叫作Zygote的進程fork出來的。Zygote進程在系統啓動而且載入通用的framework的代碼與資源以後開始啓動。爲了啓動一個新的程序進程,系統會fork Zygote進程生成一個新的進程,而後在新的進程中加載並運行應用程序的代碼。這使得大多數的RAM pages被用來分配給framework的代碼,同時使得RAM資源可以在應用的全部進程之間進行共享。
    2. 大多數static的數據被mmapped到一個進程中。這不只僅使得一樣的數據可以在進程間進行共享,並且使得它可以在須要的時候被paged out。常見的static數據包括Dalvik Code,app resources,so文件等。
    3. 大多數狀況下,Android經過顯式的分配共享內存區域(例如ashmem或者gralloc)來實現動態RAM區域可以在不一樣進程之間進行共享的機制。例如,Window Surface在App與Screen Compositor之間使用共享的內存,Cursor Buffers在Content Provider與Clients之間共享內存。
  • 5.3分配與回收內存

    1. 每個進程的Dalvik heap都反映了使用內存的佔用範圍。這就是一般邏輯意義上提到的Dalvik Heap Size,它能夠隨着須要進行增加,可是增加行爲會有一個系統爲它設定的上限。
    2. 邏輯上講的Heap Size和實際物理意義上使用的內存大小是不對等的,Proportional Set Size(PSS)記錄了應用程序自身佔用以及和其餘進程進行共享的內存。
  • 5.4限制應用的內存

    1. 爲了整個Android系統的內存控制須要,Android系統爲每個應用程序都設置了一個硬性的Dalvik Heap Size最大限制閾值,這個閾值在不一樣的設備上會由於RAM大小不一樣而各有差別。若是你的應用佔用內存空間已經接近這個閾值,此時再嘗試分配內存的話,很容易引發OutOfMemoryError的錯誤。
    2. ActivityManager.getMemoryClass()能夠用來查詢當前應用的Heap Size閾值,這個方法會返回一個整數,代表你的應用的Heap Size閾值是多少Mb(megabates)。
  • 5.5應用切換

    1. Android系統並不會在用戶切換應用的時候作交換內存的操做。Android會把那些不包含Foreground組件的應用進程放到LRU Cache中。例如,當用戶開始啓動了一個應用,系統會爲它建立了一個進程,可是當用戶離開這個應用,此進程並不會當即被銷燬,而是會被放到系統的Cache當中,若是用戶後來再切換回到這個應用,此進程就可以被立刻完整的恢復,從而實現應用的快速切換。
    2. 若是你的應用中有一個被緩存的進程,這個進程會佔用必定的內存空間,它會對系統的總體性能有影響。所以當系統開始進入Low Memory的狀態時,它會由系統根據LRU的規則與應用的優先級,內存佔用狀況以及其餘因素的影響綜合評估以後決定是否被殺掉。

須要特別注意的:

  • 在Dalvik下,大部分Davik採起的都是標記-清理回收算法,並且具體使用什麼算法是在編譯期決定的,沒法在運行的時候動態更換。標記-清理回收算法沒法對Heap中空閒內存區域作碎片整理。系統僅僅會在新的內存分配以前判斷Heap的尾端剩餘空間是否足夠,若是空間不夠會觸發gc操做,從而騰出更多空閒的內存空間;這樣內存空洞就產生了。

內存碎片的產生

如上圖所示, 第一行,在開始階段,內存分配較滿;第二行,通過GC以後,大部分對象被釋放。此時可能產生的問題是,由於沒有內存整理功能,整個頁面的4KB內存(內存分配的最小單位是頁面,一般爲4KB)可能只有一個小對象,可是統計PrivateDirty/Pss時仍是按照4KB計算。因此對於Dalvik虛擬機的手機來講,咱們首先要儘可能避免掉頻繁生成不少臨時小變量(好比說:getView, onDraw等函數中new對象),另外一個又要儘可能去避免產生不少長生命週期的大對象。

  • ART在GC上不像Dalvik僅有一種回收算法,ART在不一樣的狀況下會選擇不一樣的回收算法。應用程序在前臺運行時,響應性是最重要的,所以也要求執行的GC是高效的。相反,應用程序在後臺運行時,響應性不是最重要的,這時候就適合用來解決堆的內存碎片問題。所以,Mark-Sweep GC適合做爲Foreground GC,而Mark-Compact GC適合做爲Background GC。因爲有Compact的能力存在,內存碎片在ART上能夠很好的被避免,這個也是ART一個很好的能力。

6、Android GC什麼時候發生?

由上文咱們知道,GC操做主要是由系統決定的,可是咱們能夠監聽系統的GC過程,以此來分析咱們應用程序當前的內存狀態。
Dalvik虛擬機,每一次GC打印內容格式:

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>複製代碼

含義解析

  • GC Reason:GC觸發緣由
    GC_CONCURRENT:當已分配內存達到某一值時,觸發併發GC;
    GC_FOR_MALLOC:當嘗試在堆上分配內存不足時觸發的GC;系統必須中止應用程序並回收內存;
    GC_HPROF_DUMP_HEAP: 當須要建立HPROF文件來分析堆內存時觸發的GC;
    GC_EXPLICIT:當明確的調用GC時,例如調用System.gc()或者經過DDMS工具顯式地告訴系統進行GC操做等;
    GC_EXTERNAL_ALLOC: 僅在API級別爲10或者更低時(新版本分配內存都在Dalvik堆上)
  • Amount freed GC:回收的內存大小
  • Heap stats:堆上的空閒內存百分比 (已用內存)/(堆上總內存)
  • External memory stats: API級別爲10或者更低:(已分配的內存量)/ (即將發生垃圾的極限)
  • Pause time:此次GC操做致使應用程序暫停的時間。關於這個暫停的時間,在2.3以前GC操做是不能併發進行的,也就是系統正在進行GC,那麼應用程序就只能阻塞住等待GC結束。而自2.3以後,GC操做改爲了併發的方式進行,就是說GC的過程當中不會影響到應用程序的正常運行,可是在GC操做的開始和結束的時候會短暫阻塞一段時間。

Art虛擬機,每一次GC打印內容格式:

I/art:<GC_Reason><Amount_freed>,<LOS_Space_Status>,<Heap_stats>,<Pause_time>,<Total_time>複製代碼

基本狀況和Dalvik沒有什麼差異,GC的Reason更多了,還多了一個LOS_Space_Status.

LOS_Space_Status:Large Object Space,大對象佔用的空間,這部份內存並非分配在堆上的,但仍屬於應用程序內存空間,主要用來管理 Bitmap 等佔內存大的對象,避免因分配大內存致使堆頻繁 GC。

7、獲取內存使用狀況

經過命令行adb shell dumpsys meminfo packagename查看內存詳細佔用狀況:

命令行查看內存分配狀況
其中幾個關鍵的數據:

  • Private(Clean和Dirty的):應用進程單獨使用的內存,表明着系統殺死你的進程後能夠實際回收的內存總量**。一般須要特別關注其中更爲昂貴的dirty部分,它不只只被你的進程使用並且會持續佔用內存而不能被從內存中置換出存儲。申請的所有Dalvik和本地heap內存都是Dirty的,和Zygote共享的Dalvik和本地heap內存也都是Dirty的。
  • Dalvik Heap:Dalvik虛擬機使用的內存,包含dalvik-heap和dalvik-zygote,堆內存,全部的Java對象實例都放在這裏。
  • Heap Alloc:累加了Dalvik和Native的heap。
  • PSS:這是加入與其餘進程共享的分頁內存後你的應用佔用的內存量,你的進程單獨使用的所有內存也會加入這個值裏,多進程共享的內存按照共享比例添加到PSS值中。如一個內存分頁被兩個進程共享,每一個進程的PSS值會包括此內存分頁大小的一半在內。
    Dalvik Pss內存 = 私有內存Private Dirty + (共享內存Shared Dirty / 共享進程數)
  • TOTAL:上面所有條目的累加值,全局的展現了你的進程佔用的內存狀況。
  • ViewRootImpl:應用進程裏的活動窗口視圖個數,能夠用來監測對話框或者其餘窗口的內存泄露。
  • AppContexts及Activities:應用進程裏Context和Activity的對象個數,能夠用來監測Activity的內存泄露。

參考:

歡迎關注微信公衆號:按期分享Java、Android乾貨!

歡迎關注
相關文章
相關標籤/搜索