Android GC 那點事

版權聲明:本文由陳昱全原創文章,轉載請註明出處: 
文章原文連接:https://www.qcloud.com/community/article/170算法

來源:騰雲閣 https://www.qcloud.com/community網絡

 

想寫一篇關於Android GC的想法來源於追查一個魅族手機圖片滑動卡頓問題,因爲不斷的GC致使的丟幀卡頓的問題讓咱們想了不少方案去解決,因此就打算詳細的看看內存分配和GC的原理,爲何會不斷的GC, GC ALLOC和GC COCURRENT有什麼區別,能不能想辦法擴大堆內存減小GC的頻次等等。併發

1. JVM內存回收機制

1.1. 回收算法

  • 標記回收算法(Mark and Sweep GC)
    從"GC Roots"集合開始,將內存整個遍歷一次,保留全部能夠被GC Roots直接或間接引用到的對象,而剩下的對象都看成垃圾對待並回收,這個算法須要中斷進程內其它組件的執行而且可能產生內存碎片。
  • 複製算法 (Copying)
    將現有的內存空間分爲兩快,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象複製到未被使用的內存塊中,以後,清除正在使用的內存塊中的全部對象,交換兩個內存的角色,完成垃圾回收。
  • 標記-壓縮算法 (Mark-Compact)
    先須要從根節點開始對全部可達對象作一次標記,但以後,它並不簡單地清理未標記的對象,而是將全部的存活對象壓縮到內存的一端。以後,清理邊界外全部的空間。這種方法既避免了碎片的產生,又不須要兩塊相同的內存空間,所以,其性價比比較高。
  • 分代
    將全部的新建對象都放入稱爲年輕代的內存區域,年輕代的特色是對象會很快回收,所以,在年輕代就選擇效率較高的複製算法。當一個對象通過幾回回收後依然存活,對象就會被放入稱爲老生代的內存空間。對於新生代適用於複製算法,而對於老年代則採起標記-壓縮算法。

1.2. 複製和標記-壓縮算法的區別

乍一看這兩個算法彷佛並無多大的區別,都是標記了而後挪到另外的內存地址進行回收,那爲何不一樣的分代要使用不一樣的回收算法呢?函數

其實2者最大的區別在於前者是用空間換時間後者則是用時間換空間。性能

前者的在工做的時候是不沒有獨立的「Mark」與「Copy」階段的,而是合在一塊兒作一個動做,就叫Scavenge(或Evacuate,或者就叫Copy)。也就是說,每發現一個此次收集中還沒有訪問過的活對象就直接Copy到新地方,同時設置Forwarding Pointer,這樣的工做方式就須要多一份空間。優化

後者在工做的時候則須要分別的Mark與Compact階段,Mark階段用來發現並標記全部活的對象,而後compact階段才移動對象來達到Compact的目的。若是Compact方式是Sliding Compaction,則在Mark以後就能夠按順序一個個對象「滑動」到空間的某一側。由於已經先遍歷了整個空間裏的對象圖,知道全部的活對象了,因此移動的時候就能夠在同一個空間內而不須要多一份空間。spa

因此新生代的回收會更快一點,老年代的回收則會須要更長時間,同時壓縮階段是會暫停應用的,因此給咱們應該儘可能避免對象出如今老年代。線程

2. Dalvik虛擬機

2.1. Java堆

Java堆其實是由一個Active堆和一個Zygote堆組成的,其中,Zygote堆用來管理Zygote進程在啓動過程當中預加載和建立的各類對象,而Active堆是在Zygote進程Fork第一個子進程以前建立的。之後啓動的全部應用程序進程是被Zygote進程Fork出來的,並都持有一個本身的Dalvik虛擬機。在建立應用程序的過程當中,Dalvik虛擬機採用Cow策略複製Zygote進程的地址空間。日誌

Cow策略:一開始的時候(未複製Zygote進程的地址空間的時候),應用程序進程和Zygote進程共享了同一個用來分配對象的堆。當Zygote進程或者應用程序進程對該堆進行寫操做時,內核就會執
行真正的拷貝操做,使得Zygote進程和應用程序進程分別擁有本身的一份拷貝,這就是所謂的Cow。由於Copy是十分耗時的,因此必須儘可能避免Copy或者儘可能少的Copy。code

爲了實現這個目的,當建立第一個應用程序進程時,會將已經使用了的那部分堆內存劃分爲一部分,尚未使用的堆內存劃分爲另一部分。前者就稱爲Zygote堆,後者就稱爲Active堆。這樣只需把zygote堆中的內容複製給應用程序進程就能夠了。之後不管是Zygote進程,仍是應用程序進程,當它們須要分配對象的時候,都在Active堆上進行。這樣就可使得Zygote堆儘量少地被執行寫操做,於是就能夠減小執行寫時拷貝的操做。在Zygote堆裏面分配的對象其實主要就是Zygote進程在啓動過程當中預加載的類、資源和對象了。這意味着這些預加載的類、資源和對象能夠在Zygote進程和應用程序進程中作到長期共享。這樣既能減小拷貝操做,還能減小對內存的需求。

2.2. 和GC有關的一些指標

記得咱們以前在優化魅族某手機的gc卡頓問題時,發現他很容易觸發GC_FOR_MALLOC,這個GC類別後續會說到,是分配對象內存不足時致使的。但是咱們又設置了很大的堆Size爲何還會內存不夠呢,這裏須要瞭解如下幾個概念:分別是Java堆的起始大小(Starting Size)、最大值(Maximum Size)和增加上限值(Growth Limit)。
在啓動Dalvik虛擬機的時候,咱們能夠分別經過-Xms、-Xmx和-XX:HeapGrowthLimit三個選項來指定上述三個值,以上三個值分別表示表示:

  • Starting Size: Dalvik虛擬機啓動的時候,會先分配一塊初始的堆內存給虛擬機使用。
  • Growth Limit: 是系統給每個程序的最大堆上限,超過這個上限,程序就會OOM。
  • Maximum Size: 不受控狀況下的最大堆內存大小,起始就是咱們在用largeheap屬性的時候,能夠從系統獲取的最大堆大小。

同時除了上面的這個三個指標外,還有幾個指標也是值得咱們關注的,那就是堆最小空閒值(Min Free)、堆最大空閒值(Max Free)和堆目標利用率(Target Utilization)。假設在某一次GC以後,存活對象佔用內存的大小爲LiveSize,那麼這時候堆的理想大小應該爲(LiveSize / U)。可是(LiveSize / U)必須大於等於(LiveSize + MinFree)而且小於等於(LiveSize + MaxFree),每次GC後垃圾回收器都會盡可能讓堆的利用率往目標利用率靠攏。因此當咱們嘗試手動去生成一些幾百K的對象,試圖去擴大可用堆大小的時候,反而會致使頻繁的GC,由於這些對象的分配會致使GC,而GC後會讓堆內存回到合適的比例,而咱們使用的局部變量很快會被回收理論上存活對象仍是那麼多,咱們的堆大小也會縮減回來沒法達到擴充的目的。 與此同時這也是產生CONCURRENT GC的一個因素,後文咱們會詳細講到。

2.3. GC的類型

  • GC_FOR_MALLOC: 表示是在堆上分配對象時內存不足觸發的GC。
  • GC_CONCURRENT: 當咱們應用程序的堆內存達到必定量,或者能夠理解爲快要滿的時候,系統會自動觸發GC操做來釋放內存。
  • GC_EXPLICIT: 表示是應用程序調用System.gc、VMRuntime.gc接口或者收到SIGUSR1信號時觸發的GC。
  • GC_BEFORE_OOM: 表示是在準備拋OOM異常以前進行的最後努力而觸發的GC。

實際上,GC_FOR_MALLOC、GC_CONCURRENT和GC_BEFORE_OOM三種類型的GC都是在分配對象的過程觸發的。而併發和非併發GC的區別主要在於前者在GC過程當中,有條件地掛起和喚醒非GC線程,然後者在執行GC的過程當中,一直都是掛起非GC線程的。並行GC經過有條件地掛起和喚醒非GC線程,就可使得應用程序得到更好的響應性。可是同時並行GC須要多執行一次標記根集對象以及遞歸標記那些在GC過程被訪問了的對象的操做,因此也須要花費更多的CPU資源。後文在ART的併發和非併發GC中咱們也會着重說明下這二者的區別。

2.4. 對象的分配和GC觸發時機

  1. 調用函數dvmHeapSourceAlloc在Java堆上分配指定大小的內存。若是分配成功,那麼就將分配獲得的地址直接返回給調用者了。函數dvmHeapSourceAlloc在不改變Java堆當前大小的前提下進行內存分配,這是屬於輕量級的內存分配動做。
  2. 若是上一步內存分配失敗,這時候就須要執行一次GC了。不過若是GC線程已經在運行中,即gDvm.gcHeap->gcRunning的值等於true,那麼就直接調用函數dvmWaitForConcurrentGcToComplete等到GC執行完成就是了。不然的話,就須要調用函數gcForMalloc來執行一次GC了,參數false表示不要回收軟引用對象引用的對象。
  3. GC執行完畢後,再次調用函數dvmHeapSourceAlloc嘗試輕量級的內存分配操做。若是分配成功,那麼就將分配獲得的地址直接返回給調用者了。
  4. 若是上一步內存分配失敗,這時候就得考慮先將Java堆的當前大小設置爲Dalvik虛擬機啓動時指定的Java堆最大值,再進行內存分配了。這是經過調用函數dvmHeapSourceAllocAndGrow來實現的。
  5. 若是調用函數dvmHeapSourceAllocAndGrow分配內存成功,則直接將分配獲得的地址直接返回給調用者了。
  6. 若是上一步內存分配仍是失敗,這時候就得出狠招了。再次調用函數gcForMalloc來執行GC。參數true表示要回收軟引用對象引用的對象。
  7. GC執行完畢,再次調用函數dvmHeapSourceAllocAndGrow進行內存分配。這是最後一次努力了,成功與事都到此爲止。
    示例圖以下:

經過這個流程能夠看到,在對象的分配中會致使GC,第一次分配對象失敗咱們會觸發GC可是不回收Soft的引用,若是再次分配仍是失敗咱們就會將Soft的內存也給回收,前者觸發的GC是GC_FOR_MALLOC類型的GC,後者是GC_BEFORE_OOM類型的GC。而當內存分配成功後,咱們會判斷當前的內存佔用是不是達到了GC_CONCURRENT的閥值,若是達到了那麼又會觸發GC_CONCURRENT。
那麼這個閥值又是如何來的呢,上面咱們說到的一個目標利用率,GC後咱們會記錄一個目標值,這個值理論上須要再上述的範圍以內,若是不在咱們會選取邊界值作爲目標值。虛擬機會記錄這個目標值,當作當前容許總的能夠分配到的內存。同時根據目標值減去固定值(200~500K), 當作觸發GC_CONCURRENT事件的閾值。

2.5. 回收算法和內存碎片

主流的大部分Davik採起的都是標註與清理(Mark and Sweep)回收算法,也有實現了拷貝GC的,這一點和HotSpot是不同的,具體使用什麼算法是在編譯期決定的,沒法在運行的時候動態更換。若是在編譯dalvik虛擬機的命令中指明瞭"WITH_COPYING_GC"選項,則編譯"/dalvik/vm/alloc/Copying.cpp"源碼 – 此是Android中拷貝GC算法的實現,不然編譯"/dalvik/vm/alloc/HeapSource.cpp" – 其實現了標註與清理GC算法。
因爲Mark and Sweep算法的缺點,容易致使內存碎片,因此在這個算法下,當咱們有大量不連續小內存的時候,再分配一個較大對象時,仍是會很是容易致使GC,好比咱們在該手機上decode圖片,具體狀況以下:

因此對於Dalvik虛擬機的手機來講,咱們首先要儘可能避免掉頻繁生成不少臨時小變量(好比說:getView, onDraw等函數中new對象),另外一個又要儘可能去避免產生不少長生命週期的大對象。

3. ART內存回收機制

3.1. Java堆

ART運行時內部使用的Java堆的主要組成包括Image Space、Zygote Space、Allocation Space和Large Object Space四個Space,Image Space用來存在一些預加載的類, Zygote Space和Allocation Space與Dalvik虛擬機垃圾收集機制中的Zygote堆和Active堆的做用是同樣的,
Large Object Space就是一些離散地址的集合,用來分配一些大對象從而提升了GC的管理效率和總體性能,相似以下圖:

在下文的GC Log中,咱們也能看到在ART的GC Log中包含了LOS的信息,方便咱們查看大內存的狀況。

3.2. GC的類型

kGcCauseForAlloc: 當要分配內存的時候發現內存不夠的狀況下引發的GC,這種狀況下的GC會Stop World.
kGcCauseBackground: 當內存達到必定的閥值的時候會去出發GC,這個時候是一個後臺GC,不會引發Stop World.
kGcCauseExplicit,顯示調用的時候進行的gc,若是ART打開了這個選項的狀況下,在system.gc的時候會進行GC.
其餘更多。

3.3. 對象的分配和GC觸發時機

因爲ART下內存分配和Dalvik下基本沒有任何區別,我直接貼圖帶過了。

3.4. 併發和非併發GC

ART在GC上不像Dalvik僅有一種回收算法,ART在不一樣的狀況下會選擇不一樣的回收算法,好比Alloc內存不夠的時候會採用非併發GC,而在Alloc後發現內存達到必定閥值的時候又會觸發併發GC。同時在先後臺的狀況下GC策略也不盡相同,後面咱們會一一給你們說明。

  • 非併發GC
    步驟1. 調用子類實現的成員函數InitializePhase執行GC初始化階段。
    步驟2. 掛起全部的ART運行時線程。
    步驟3. 調用子類實現的成員函數MarkingPhase執行GC標記階段。
    步驟4. 調用子類實現的成員函數ReclaimPhase執行GC回收階段。
    步驟5. 恢復第2步掛起的ART運行時線程。
    步驟6. 調用子類實現的成員函數FinishPhase執行GC結束階段。
  • 併發GC
    步驟1. 調用子類實現的成員函數InitializePhase執行GC初始化階段。
    步驟2. 獲取用於訪問Java堆的鎖。
    步驟3. 調用子類實現的成員函數MarkingPhase執行GC並行標記階段。
    步驟4. 釋放用於訪問Java堆的鎖。
    步驟5. 掛起全部的ART運行時線程。
    步驟6. 調用子類實現的成員函數HandleDirtyObjectsPhase處理在GC並行標記階段被修改的對象。
    步驟7. 恢復第4步掛起的ART運行時線程。
    步驟8. 重複第5到第7步,直到全部在GC並行階段被修改的對象都處理完成。
    步驟9. 獲取用於訪問Java堆的鎖。
    步驟10. 調用子類實現的成員函數ReclaimPhase執行GC回收階段。
    步驟11. 釋放用於訪問Java堆的鎖。
    步驟12. 調用子類實現的成員函數FinishPhase執行GC結束階段。
    因此不管是併發仍是非併發,都會引發Stop World的狀況出現,併發的狀況下單次Stop World的時間會更短,基本區別和Dalvik相似。

    3.5. ART併發和Dalvik併發GC的差別

    首先能夠經過以下2張圖來對比下。
    Dalvik GC:

    ART GC:

ART的併發GC和Dalvik的併發GC有什麼區別呢,初看好像2者差很少,雖然沒有一直掛起線程,可是也會有暫停線程去執行標記對象的流程。經過閱讀相關文檔能夠了解到ART併發GC對於Dalvik來講主要有三個優點點:

  1. 標記自身
    ART在對象分配時會將新分配的對象壓入到Heap類的成員變量allocationstack描述的Allocation Stack中去,從而能夠必定程度上縮減對象遍歷範圍。
  2. 預讀取
    對於標記Allocation Stack的內存時,會預讀取接下來要遍歷的對象,同時再取出來該對象後又會將該對象引用的其餘對象壓入棧中,直至遍歷完畢。
  3. 減小Suspend時間
    在Mark階段是不會Block其餘線程的,這個階段會有髒數據,好比Mark發現不會使用的可是這個時候又被其餘線程使用的數據,在Mark階段也會處理一些髒數據而不是留在最後Block的時候再去處理,這樣也會減小後面Block階段對於髒數據的處理的時間。

3.6. 先後臺GC

前臺Foreground指的就是應用程序在前臺運行時,然後臺Background就是應用程序在後臺運行時。所以,Foreground GC就是應用程序在前臺運行時執行的GC,而Background就是應用程序在後臺運行時執行的GC。
應用程序在前臺運行時,響應性是最重要的,所以也要求執行的GC是高效的。相反,應用程序在後臺運行時,響應性不是最重要的,這時候就適合用來解決堆的內存碎片問題。所以,Mark-Sweep GC適合做爲Foreground GC,而Mark-Compact GC適合做爲Background GC。
因爲有Compact的能力存在,碎片化在ART上能夠很好的被避免,這個也是ART一個很好的能力。

3.7. ART大法好

總的來看,ART在GC上作的比Dalvik好太多了,不光是GC的效率,減小Pause時間,並且還在內存分配上對大內存的有單獨的分配區域,同時還能有算法在後臺作內存整理,減小內存碎片。對於開發者來講ART下咱們基本能夠避免不少相似GC致使的卡頓問題了。另外根據谷歌本身的數據來看,ART相對Dalvik內存分配的效率提升了10倍,GC的效率提升了2-3倍。

4. GC Log

當咱們想要根據GC日誌來追查一些GC可能形成的卡頓時,咱們須要瞭解GC日誌的組成,不一樣信息表明了什麼含義。

4.1. Dalvik GC日誌

Dalvik的日誌格式基本以下:
D/dalvikvm:<GC_Reason><Amount_freed>,<Heap_stats>,<Pause_time>,<Total_time>
GC_Reason: 就是咱們上文提到的,是gc_alloc仍是gc_concurrent,瞭解到不一樣的緣由方便咱們作不一樣的處理。
Amount_freed: 表示系統經過此次GC操做釋放了多少內存。
Heap_stats: 中會顯示當前內存的空閒比例以及使用狀況(活動對象所佔內存 / 當前程序總內存)。
Pause_time: 表示此次GC操做致使應用程序暫停的時間。關於這個暫停的時間,在2.3以前GC操做是不能併發進行的,也就是系統正在進行GC,那麼應用程序就只能阻塞住等待GC結束。而自2.3以後,GC操做改爲了併發的方式進行,就是說GC的過程當中不會影響到應用程序的正常運行,可是在GC操做的開始和結束的時候會短暫阻塞一段時間,因此還有後續的一個total_time。
Total_time: 表示本次GC所花費的總時間和上面的Pause_time,也就是stop all是不同的,卡頓時間主要看上面的pause_time。

4.2. ART GC日誌

I/art:<GC_Reason><Amount_freed>,<LOS_Space_Status>,<Heap_stats>,<Pause_time>,<Total_time>
基本狀況和Dalvik沒有什麼差異,GC的Reason更多了,還多了一個OS_Space_Status.

LOS_Space_Status:Large Object Space,大對象佔用的空間,這部份內存並非分配在堆上的,但仍屬於應用程序內存空間,主要用來管理 bitmap 等佔內存大的對象,避免因分配大內存致使堆頻繁 GC。 寫在最後:圖片來源自網絡,特別鳴謝羅昇陽。

相關文章
相關標籤/搜索