導語:
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應用程序的開發語言,能夠說是技術與商業之間一個折衷,事實證實,這種折衷是成功的。
數組
在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,以即可以實現上述的非遞歸函數調用的遞歸標記算法。