5-Dalvik垃圾收集機制Cocurrent GC

Dalivik垃圾回收收機制Cocurrent GC簡介和學習計劃

導語:
java


在C/C++中,開發者須要手動地管理在堆中分配的內存,可是這每每致使不少問題。android

一、 內存分配以後忘記釋放,形成內存泄漏。ios

二、 非法訪問那些已經釋放了的內存,引起程序崩潰。算法


沒有一個好的C/C++應用程序開發框架,通常的開發者根本沒法駕馭內存問題,由於程序大了以後,很容易形成失控。最要命的是,內存被破壞的時候,並不必定就是程序崩潰的時候,它就是一顆不定時炸彈,說不許何時會被引爆,查找緣由也是很是困難的。Java 語言運行在虛擬機上,虛擬機能夠自動回收那些再也不使用了的Java Object,也就是那些再也不被引用了的Java Object。 這就是Java語言的一種重要特性--垃圾自動收集機制。  垃圾回收機制將開發者從內存問題中解放出來,極大地提升了開發效率,以及提升了程序的可維護性。這也是Android爲何會選擇Java而不是C/C++來做爲主要應用程序開發語言的緣由之一。就是爲了可以讓開發遠離內存問題,而將精力集中在業務上,開發出更多更好的APP來,從而迎頭趕超iOS。Android系統內存也存在大量的C/C++代碼,這隻要考慮性能問題, 不過,爲了不出現內存問題,在Android系統內部的C++代碼,大量地使用了智能指針來自動管理對象的生命週期。選擇Java來做爲Android應用程序的開發語言,能夠說是技術與商業之間一個折衷,事實證實,這種折衷是成功的。 
數組


Android 垃圾回收機制簡史:


在GingerBread(android2.3)以前,Dalvik虛擬使用的垃圾收集機制有如下特色:數據結構

1. Stop-the-world,也就是垃圾收集線程在執行的時候,其它的線程都中止;併發

2. Full heap collection,也就是一次收集徹底部的垃圾;框架

3. 一次垃圾收集形成的程序停止時間一般都大於100ms。函數


GingerBread(android2.3)---Kit Kat(4.4),Dalvik虛擬使用的垃圾收集機制獲得了改進性能

1.  Cocurrent GC : 也就是大多數狀況下,垃圾收集線程與其它線程是併發執行的

2.  Partial collection,也就是一次可能只收集一部分垃圾;

3.  一次垃圾收集形成的程序停止時間一般都小於5ms。


Kit Kat(4.4以上版本),Android開始使用ART替代Dalivk虛擬機, ART的垃圾回收機制有一次作了優化

特色和Dalivik基本一致,效率上比Dalivik更優!!!



Dalivk的堆管理

                     圖1 Dalvik虛擬機垃圾收集機制的基本概念
        
一、Dalivik堆: 
全部的java對象都是在Dalivik堆上面申請的, Dalivik堆分爲兩部分。 Active Heap 和 Zygote Heap。 事實上,Dalvik虛擬機的堆最初是隻有一個的,也就是Zygote進程在啓動過程當中建立Dalvik虛擬機的時候,只有一個堆。 可是當Zygote進程在fork第一個應用程序進程以前,會將已經使用了的那部分堆內存劃分爲一部分,尚未使用的堆內存劃分爲另一部分。前者就稱爲Zygote堆,後者就稱爲Active堆

二、Heap Bitmap: 
堆位圖,用於記錄android中全部應用程序的Java 對象的引用狀況。 兩個Bitmap來描述堆的對象的狀態。 一個稱爲Live Bitmap,另外一個稱爲Mark Bitmap。 Heap Bitmap使用位圖來標記對象是否被使用。若是一個對象被引用,那麼在Bitmap中與它對應的那一位就會被設置爲1。不然的話,就設置爲0。 Live Bitmap用來標記上一次GC時被引用的對象,也就是沒有被回收的對象,而Mark Bitmap用來標記當前GC有被引用的對象。 有了這兩個信息以後,咱們就能夠很容易地知道哪些對象是須要被回收的,即在Live Bitmap在標記爲1,可是在Mark Bitmap中標記爲0的對象。 

三、Card Table: 
在垃圾收集的Mark階段,要求除了垃圾收集線程以外,其它的線程都中止,不然的話,就會可能致使不能正確地標記每個對象。這種現象在垃圾收集算法中稱爲Stop The World,會致使程序停止執行,形成停頓的現象。爲了儘量地減小停頓,咱們必需要容許在Mark階段有條件地容許程序的其它線程執行。這種垃圾收集算法稱爲並行垃圾收集算法Concurrent GC。爲了實現Concurrent GC,Mark階段又劃分兩個子階段。

四、Mark Stack:
在Mark階段,Dalvik虛擬機能過遞歸方式來標記對象。可是,這不是經過函數的遞歸調用來實現的,而是藉助一個稱爲Mark Stack的棧來實現的。

1. 爲何要把用來分配對象的堆劃分爲Active堆和Zygote堆 ?
Android系統的第一個Dalvik虛擬機是由Zygote進程建立的。  應用程序進程是由Zygote進程fork出來的。也就是說,應用程序進程使用了一種寫時拷貝技術(COW)來複制了Zygote進程的地址空間。這意味着一開始的時候,應用程序進程和Zygote進程共享了同一個用來分配對象的堆。 然而,當Zygote進程或者應用程序進程對該堆進行寫操做時,內核就會執行真正的拷貝操做,使得Zygote進程和應用程序進程分別擁有本身的一份拷貝。拷貝是一件費時費力的事情。 所以,爲了儘可能地避免拷貝,Dalvik虛擬機將本身的堆劃分爲兩部分。事實上,Dalvik虛擬機的堆最初是隻有一個的。也就是Zygote進程在啓動過程當中建立Dalvik虛擬機的時候,只有一個堆。 可是當Zygote進程在fork第一個應用程序進程以前,會將已經使用了的那部分堆內存劃分爲一部分,尚未使用的堆內存劃分爲另一部分。前者就稱爲Zygote堆,後者就稱爲Active堆。 之後不管是Zygote進程,仍是應用程序進程,當它們須要分配對象的時候,都在Active堆上進行。這樣就可使得Zygote堆儘量少地被執行寫操做,於是就能夠減小執行寫時拷貝的操做。

在Zygote堆裏面分配的對象其實主要就是Zygote進程在啓動過程當中預加載的類、資源和對象了。這意味着這些預加載的類、資源和對象能夠在Zygote進程和應用程序進程中作到長期共享。這樣既能減小拷貝操做,還能減小對內存的需求。 


2. 堆/堆管理
     圖2 Dalvik虛擬機的堆
       

在Dalvik虛擬機中,堆實際上就是一塊匿名共享內存。Dalvik虛擬機並非直接管理這塊匿名共享內存,而是將它封裝成一個mspace,交給C庫來管理。 mspace 是libc中的概念,咱們能夠經過libc提供的函數create_mspace_with_base建立一個mspace,而後再經過mspace_開頭的函數管理該mspace。 例如,咱們能夠經過mspace_malloc和mspace_bulk_free來在指定的mspace中分配和釋放內存。  實際上,咱們在使用libc提供的函數malloc和free分配和釋放內存時,也是在一個mspace進行的,只不過這個mspace是由libc默認建立的。 Dalvik虛擬機除了要給應用層分配對象以外,最重要的仍是要對這些已經分配出去的對象進行管理,也就是要在對象再也不被使用的時候,對其進行自動回收。

    
    
GC回收原理

Dalvik虛擬機執行完成一次垃圾收集以後,咱們一般能夠看到相似如下的日誌輸出:

D/dalvikvm(9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms  copy

在這一行日誌中,
一、 GC_CONCURRENT表示GC緣由
二、 2049K表示總共回收的內存
三、 3571K/9991K表示Java Object Heap統計,即在9991K的Java Object Heap中,有3571K是正在使用的
四、 4703K/5261K表示External Memory統計,即在5261K的External Memory中,有4703K是正在使用的

五、 2ms+2ms表示垃圾收集形成的程序停止時間。

Dalivk垃圾收集的使用的 耳熟能詳,大名鼎鼎的的Mark-Sweep算法。  Mark-Sweep垃圾收集算法主要分爲兩個階段:Mark和Sweep。
1.Mark階段從對象的根集開始標記 被引用的對象 。標記完成後,就進入到Sweep階段
2.Sweep階段所作的事情就是回收沒有被標記的對象佔用的內存。

下面咱們來介紹一下在 Mark和Sweep 過程當中 堆管理的結構體的做用

1.Bitmap:
這裏涉及到的一個核心概念就是咱們怎麼標記對象有沒有被引用的,換句說就是經過什麼 數據結構 來描述對象有沒有被引用。是的,就是圖1中的Heap Bitmap了。Heap Bitmap的結構如圖3所示:

                             圖3 Heap Bitmap

一、在Dalvik虛擬機中, 使用一個unsigned long數組 來描述一個Heap Bitmap。 
二、咱們使用libc提供的函數mspace_malloc來從堆裏面分配內存時,獲得的內存的地址老是對齊到HB_OBJECT_ALIGNMENT(8)的 ,也就是說,咱們分配的對象的地址的最低三位老是0。 爲了減小 Bitmap的大小。  Bitmap中的位與對象的對應關係時,忽略最低三位。

2.Card Table:
在垃圾收集的Mark階段,要求除了垃圾收集線程以外,其它的線程都中止,不然的話,就會可能致使不能正確地標記每個對象。這種現象在垃圾收集算法中稱爲Stop The World,會致使程序停止執行,形成停頓的現象。 爲了儘量地減小停頓,咱們必需要容許在Mark階段有條件地容許程序的其它線程執行。這種垃圾收集算法稱爲並行垃圾收集算法Concurrent GC。
爲了實現Concurrent GC,Mark階段又劃分兩個子階段。
一、第一階段:只負責標記根集對象。所謂的根集對象就是指在GC開始的瞬間,被全局變量、棧變量和寄存器等引用的對象
二、 第二階段:負責 標記被根集對象引用的對象的過程就是。 有了這些根集變量以後,咱們就能夠順着它們找到其他的被引用變量。例如,一個棧變量引了一個對象,而這個對象又經過成員變量引用了另一個對象,那該被引用的對象也會同時標記爲正在使用。

在Concurrent GC,第一個子階段是不容許垃圾收集線程以外的線程運行的,可是第二個子階段是容許的。 不過,在第二個子階段執行的過程當中,若是一個線程修改了一個對象,那麼該對象必需要記錄起來,由於它頗有可能引用了新的對象,或者引用了以前未引用過的對象。若是不這樣作的話,那麼就會致使被引用對象還在使用然而卻被回收。 這種狀況出如今只進行部分垃圾收集的狀況,這時候Card Table的做用就是用來記錄非垃圾收集堆對象對垃圾收集堆對象的引用。

Dalvik虛擬機進行部分垃圾收集時,實際上就是隻收集在Active堆上分配的對象。 所以對Dalvik虛擬機來講,Card Table就是用來記錄在Zygote堆上分配的對象在部收垃圾收集執行過程當中對在Active堆上分配的對象的引用。        
咱們是否是想到再用一個Bitmap在描述上述第二個子階段被修改的對象呢?雖然咱們盡大努力減小了用來標記對象的Bitmap的大小,不過仍是比較可觀的。

所以,爲了減小內存的消耗,咱們使用另一種技術來標記Mark第二子階段被修改的對象。 這種技術使用到了一種稱爲Card Table的數據結構,如圖4所示:

                圖4 Card Table
       
從名字能夠看出,Card Table由Card組成,一個Card實際上就是一個字節,它的值要麼是CLEAN,要麼是DIRTY。

若是一個Card的值是CLEAN,就表示與它對應的對象在Mark第二子階段沒有被程序修改過。不然的話,就意味着被程序修改過,對於這些被修改過的對象。須要在Mark第二子階段結束以後,再次禁止垃圾收集線程以外的其它線程執行,以便垃圾收集線程再次根據Card Table記錄的信息對被修改過的對象引用的其它對象進行從新標記。
因爲Mark第二子階段執行的時間不會太長,所以在該階段被修改的對象不會不少,這樣就能夠保證第二次子階段結束後,再次執行標記對象的過程是很快的,於是此時對程序形成的停頓很是小。

在Card Table中,在連續GC_CARD_SIZE地址中的對象共用一個Card。Dalvik虛擬機將GC_CARD_SIZE的值設置爲128。所以,假設堆的大小爲Max Heap Size,那麼咱們只須要一塊字節數爲(Max Heap Size / 128)的Card Table。相比大小爲(Max Heap Size / 8 / 32)× 4的Bitmap,減小了一半的內存需求。    
    



2.Mark Stack:
在Mark階段,Dalvik虛擬機能過遞歸方式來標記對象。可是,這不是經過函數的遞歸調用來實現的,而是藉助一個稱爲Mark Stack的棧來實現的。

具體來講,當咱們標記完成根集對象以後,就按照它們的地址從小到大的順序標記它們所引用的其它對象。
假設有A、B、C和D四個對象,它的地址大小關係爲A < B < C < D,其中,B和D是根集對象,A被D引用,C沒有被B和D引用。那麼咱們將依次遍歷B和D。當遍歷到B的時候,沒有發現它引用其它對象,而後就繼續向前遍歷D對象。發現它引用了A對象。按照遞歸的算法,這時候除了標記A對象是正在使用以外,還應該去檢查A對象有沒有引用其它對象,而後又再檢查它引用的對象有沒有又引用其它的對象,一直這樣遍歷下去。這樣就跟函數遞歸同樣。 更好的作法是將對象A記錄在一個Mark Stack中,而後繼續檢查地址值比對象D大的其它對象。對於地址值比對象D大的其它對象,若是它們引用了一個地址值比它們小的其它對象,那麼這些其它對象一樣要記錄在Mark Stack中。等到該輪檢查結束以後,再回過頭來檢查記錄在Mark Stack裏面的對象。而後又重複上述過程,直到Mark Stack等於空爲止。      
   
這就是咱們在圖1中顯示的Mark Stack的做用,它的具體結構如圖5所示:

               圖5 Mark Stack
      
在Dalvik虛擬機中,每個對象都是從Object類繼承下來的,也就是說,每個對象佔用的內存大小都至少等於sizeof(Object)。 此外,咱們經過libc提供的函數 mspace_malloc 爲對象分配內存時,libc須要額外的內存來記錄被分配出去的內存的信息。 例如,須要記錄被分配出去的內存的大小。每一塊分配出去的內存須要額外的HEAP_SOURCE_CHUNK_OVERHEAD內存來記錄上述的管理信息。所以,在Dalvik虛擬機中,每個對象的大小都至少爲sizeof(Object) + HEAP_SOURCE_CHUNK_OVERHEAD。 這就意味着對於一個大小爲Max Heap Size的堆來講,最多能夠分配Max Heap Size / (sizeof(Object) + HEAP_SOURCE_CHUNK_OVERHEAD)個對象。因而,在最壞狀況下,咱們就須要一個大小爲(Max Heap Size / (sizeof(Object) + HEAP_SOURCE_CHUNK_OVERHEAD))的Object*數組來描述Mark Stack,以即可以實現上述的非遞歸函數調用的遞歸標記算法。 






相關文章
相關標籤/搜索