天天看點

Android的記憶體配置設定與回收

  想寫一篇關于android的記憶體配置設定和回收文章的想法來源于追查一個魅族手機圖檔滑動卡頓問題,我們想了很多辦法還是沒有避免他不停的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之後就可以按順序一個個對象“滑動”到空間的某一側。因為已經先周遊了整個空間裡的對象圖,知道所有的活對象了,是以移動的時候就可以在同一個空間内而不需要多一份空間。

           是以新生代的回收會更快一點,老年代的回收則會需要更長時間,同時壓縮階段是會暫停應用的,是以給我們應該盡量避免對象出現在老年代。

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。

        為了實作這個目的,當建立第一個應用程式程序時,會将已經使用了的那部分堆記憶體劃分為一部分,還沒有使用的堆記憶體劃分為另外一部分。前者就稱為zygote堆,後者就稱為active堆。這樣隻需把zygote堆中的内容複制給應用程式程序就可以了。以後無論是zygote程序,還是應用程式程序,當它們需要配置設定對象的時候,都在active堆上進行。這樣就可以使得zygote堆盡可能少地被執行寫操作,因而就可以減少執行寫時拷貝的操作。在zygote堆裡面配置設定的對象其實主要就是zygote程序在啟動過程中預加載的類、資源和對象了。這意味着這些預加載的類、資源和對象可以在zygote程序和應用程式程序中做到長期共享。這樣既能減少拷貝操作,還能減少對記憶體的需求。

2.2 對象配置設定和回收的幾個資料名額

        記得我們之前在優化魅族某手機的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後會讓堆記憶體回到合适的比例,而我們使用的局部變量很快會被回收理論上存活對象還是那麼多,我們的堆大小也會縮減回來無法達到擴充的目的。

2.3 對象的配置設定和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進行記憶體配置設定。這是最後一次努力了,成功與事都到此為止。

示例圖如下:

Android的記憶體配置設定與回收

2.4 gc的類型

gc_for_malloc:表示是在堆上配置設定對象時記憶體不足觸發的gc。

gc_concurrent:   當我們應用程式的堆記憶體達到一定量,或者可以了解為快要滿的時候,系統會自動觸發gc操作來釋放記憶體。

gc_for_malloc:   當我們的應用程式需要配置設定更多記憶體,可是現有記憶體已經不足的時候,系統會進行gc操作來釋放記憶體。

gc_explicit:表示是應用程式調用system.gc、vmruntime.gc接口或者收到sigusr1信号時觸發的gc。

gc_before_oom:表示是在準備抛oom異常之前進行的最後努力而觸發的gc。

實際上,gc_for_malloc、gc_concurrent和gc_before_oom三種類型的gc都是在配置設定對象的過程觸發的。

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圖檔,具體情況如下:

Android的記憶體配置設定與回收

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的管理效率和整體性能,類似如下圖:

Android的記憶體配置設定與回收

           在下文的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.2 并發和非并發gc

非并發gc

步驟1. 調用子類實作的成員函數initializephase執行gc初始化階段。

步驟2. 挂起所有的art運作時線程。

步驟3. 調用子類實作的成員函數markingphase執行gc标記階段。

步驟4. 調用子類實作的成員函數reclaimphase執行gc回收階段。

步驟5. 恢複第2步挂起的art運作時線程。

步驟6. 調用子類實作的成員函數finishphase執行gc結束階段。

并發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結束階段。

是以不論是并發還是非并發,都會引起stopworld的情況出現,并發的情況下單次stopworld的時間會更短。

3.4 并發gc的優勢

可以通過如下2張圖來對比下并發gc和非并發gc

非并發gc:

Android的記憶體配置設定與回收
Android的記憶體配置設定與回收
Android的記憶體配置設定與回收

3.5 前背景gc

        前台foreground指的就是應用程式在前台運作時,而背景background就是應用程式在背景運作時。是以,foreground gc就是應用程式在前台運作時執行的gc,而background就是應用程式在背景運作時執行的gc。

         應用程式在前台運作時,響應性是最重要的,是以也要求執行的gc是高效的。相反,應用程式在背景運作時,響應性不是最重要的,這時候就适合用來解決堆的記憶體碎片問題。是以,mark-sweep gc适合作為foreground gc,而mark-compact gc适合作為background gc。

3.6 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的日志格式基本如下:

Android的記憶體配置設定與回收

dalvik log

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日志

Android的記憶體配置設定與回收

art log

       基本情況和dalvik沒有什麼差别,gc的reason更多了,還多了一個os_space_status

los_space_status:large object space,大對象占用的空間,這部分記憶體并不是配置設定在堆上的,但仍屬于應用程式記憶體空間,主要用來管理 bitmap 等占記憶體大的對象,避免因配置設定大記憶體導緻堆頻繁 gc。