Dalivik垃圾回收收机制Cocurrent GC简介
在C/C++中,開發(fā)者需要手動(dòng)地管理在堆中分配的內(nèi)存,但是這往往導(dǎo)致很多問題。
1、 內(nèi)存分配之后忘記釋放,造成內(nèi)存泄漏。
2、 非法訪問那些已經(jīng)釋放了的內(nèi)存,引發(fā)程序崩潰。
沒有一個(gè)好的C/C++應(yīng)用程序開發(fā)框架,一般的開發(fā)者根本無(wú)法駕馭內(nèi)存問題,因?yàn)槌绦虼罅酥?#xff0c;很容易造成失控。最要命的是,內(nèi)存被破壞的時(shí)候,并不一定就是程序崩潰的時(shí)候,它就是一顆不定時(shí)炸彈,說(shuō)不準(zhǔn)什么時(shí)候會(huì)被引爆,查找原因也是非常困難的。Java 語(yǔ)言運(yùn)行在虛擬機(jī)上,虛擬機(jī)可以自動(dòng)回收那些不再使用了的Java Object,也就是那些不再被引用了的Java Object。 這就是Java語(yǔ)言的一種重要特性--垃圾自動(dòng)收集機(jī)制。 ?垃圾回收機(jī)制將開發(fā)者從內(nèi)存問題中解放出來(lái),極大地提高了開發(fā)效率,以及提高了程序的可維護(hù)性。這也是Android為什么會(huì)選擇Java而不是C/C++來(lái)作為主要應(yīng)用程序開發(fā)語(yǔ)言的原因之一。就是為了能夠讓開發(fā)遠(yuǎn)離內(nèi)存問題,而將精力集中在業(yè)務(wù)上,開發(fā)出更多更好的APP來(lái),從而迎頭趕超iOS。Android系統(tǒng)內(nèi)存也存在大量的C/C++代碼,這只要考慮性能問題,?不過,為了避免出現(xiàn)內(nèi)存問題,在Android系統(tǒng)內(nèi)部的C++代碼,大量地使用了智能指針來(lái)自動(dòng)管理對(duì)象的生命周期。選擇Java來(lái)作為Android應(yīng)用程序的開發(fā)語(yǔ)言,可以說(shuō)是技術(shù)與商業(yè)之間一個(gè)折衷,事實(shí)證明,這種折衷是成功的。?
Android 垃圾回收機(jī)制簡(jiǎn)史:
在GingerBread(android2.3)之前,Dalvik虛擬使用的垃圾收集機(jī)制有以下特點(diǎn):
1. Stop-the-world,也就是垃圾收集線程在執(zhí)行的時(shí)候,其它的線程都停止;
2. Full heap collection,也就是一次收集完全部的垃圾;
3. 一次垃圾收集造成的程序中止時(shí)間通常都大于100ms。
GingerBread(android2.3)---Kit Kat(4.4),Dalvik虛擬使用的垃圾收集機(jī)制得到了改進(jìn)
1. ?Cocurrent GC :?也就是大多數(shù)情況下,垃圾收集線程與其它線程是并發(fā)執(zhí)行的
2. ?Partial collection,也就是一次可能只收集一部分垃圾;
3. ?一次垃圾收集造成的程序中止時(shí)間通常都小于5ms。
Kit Kat(4.4以上版本),Android開始使用ART替代Dalivk虛擬機(jī), ART的垃圾回收機(jī)制有一次做了優(yōu)化
特點(diǎn)和Dalivik基本一致,效率上比Dalivik更優(yōu)!!!
Dalivk的堆管理
? ? ? ? ? ? ? ? ? ? ?圖1 Dalvik虛擬機(jī)垃圾收集機(jī)制的基本概念
???????? 1、Dalivik堆:? 所有的java對(duì)象都是在Dalivik堆上面申請(qǐng)的, Dalivik堆分為兩部分。Active Heap 和 Zygote Heap。?事實(shí)上,Dalvik虛擬機(jī)的堆最初是只有一個(gè)的,也就是Zygote進(jìn)程在啟動(dòng)過程中創(chuàng)建Dalvik虛擬機(jī)的時(shí)候,只有一個(gè)堆。但是當(dāng)Zygote進(jìn)程在fork第一個(gè)應(yīng)用程序進(jìn)程之前,會(huì)將已經(jīng)使用了的那部分堆內(nèi)存劃分為一部分,還沒有使用的堆內(nèi)存劃分為另外一部分。前者就稱為Zygote堆,后者就稱為Active堆
2、Heap Bitmap:? 堆位圖,用于記錄android中所有應(yīng)用程序的Java 對(duì)象的引用情況。?兩個(gè)Bitmap來(lái)描述堆的對(duì)象的狀態(tài)。 一個(gè)稱為L(zhǎng)ive Bitmap,另一個(gè)稱為Mark Bitmap。Heap Bitmap使用位圖來(lái)標(biāo)記對(duì)象是否被使用。如果一個(gè)對(duì)象被引用,那么在Bitmap中與它對(duì)應(yīng)的那一位就會(huì)被設(shè)置為1。否則的話,就設(shè)置為0。Live Bitmap用來(lái)標(biāo)記上一次GC時(shí)被引用的對(duì)象,也就是沒有被回收的對(duì)象,而Mark Bitmap用來(lái)標(biāo)記當(dāng)前GC有被引用的對(duì)象。有了這兩個(gè)信息之后,我們就可以很容易地知道哪些對(duì)象是需要被回收的,即在Live Bitmap在標(biāo)記為1,但是在Mark Bitmap中標(biāo)記為0的對(duì)象。?
3、Card Table:?
在垃圾收集的Mark階段,要求除了垃圾收集線程之外,其它的線程都停止,否則的話,就會(huì)可能導(dǎo)致不能正確地標(biāo)記每一個(gè)對(duì)象。這種現(xiàn)象在垃圾收集算法中稱為Stop The World,會(huì)導(dǎo)致程序中止執(zhí)行,造成停頓的現(xiàn)象。為了盡可能地減少停頓,我們必須要允許在Mark階段有條件地允許程序的其它線程執(zhí)行。這種垃圾收集算法稱為并行垃圾收集算法Concurrent GC。為了實(shí)現(xiàn)Concurrent GC,Mark階段又劃分兩個(gè)子階段。
4、Mark Stack: 在Mark階段,Dalvik虛擬機(jī)能過遞歸方式來(lái)標(biāo)記對(duì)象。但是,這不是通過函數(shù)的遞歸調(diào)用來(lái)實(shí)現(xiàn)的,而是借助一個(gè)稱為Mark Stack的棧來(lái)實(shí)現(xiàn)的。
1. 為什么要把用來(lái)分配對(duì)象的堆劃分為Active堆和Zygote堆 ? Android系統(tǒng)的第一個(gè)Dalvik虛擬機(jī)是由Zygote進(jìn)程創(chuàng)建的。?應(yīng)用程序進(jìn)程是由Zygote進(jìn)程fork出來(lái)的。也就是說(shuō),應(yīng)用程序進(jìn)程使用了一種寫時(shí)拷貝技術(shù)(COW)來(lái)復(fù)制了Zygote進(jìn)程的地址空間。這意味著一開始的時(shí)候,應(yīng)用程序進(jìn)程和Zygote進(jìn)程共享了同一個(gè)用來(lái)分配對(duì)象的堆。然而,當(dāng)Zygote進(jìn)程或者應(yīng)用程序進(jìn)程對(duì)該堆進(jìn)行寫操作時(shí),內(nèi)核就會(huì)執(zhí)行真正的拷貝操作,使得Zygote進(jìn)程和應(yīng)用程序進(jìn)程分別擁有自己的一份拷貝。拷貝是一件費(fèi)時(shí)費(fèi)力的事情。因此,為了盡量地避免拷貝,Dalvik虛擬機(jī)將自己的堆劃分為兩部分。事實(shí)上,Dalvik虛擬機(jī)的堆最初是只有一個(gè)的。也就是Zygote進(jìn)程在啟動(dòng)過程中創(chuàng)建Dalvik虛擬機(jī)的時(shí)候,只有一個(gè)堆。但是當(dāng)Zygote進(jìn)程在fork第一個(gè)應(yīng)用程序進(jìn)程之前,會(huì)將已經(jīng)使用了的那部分堆內(nèi)存劃分為一部分,還沒有使用的堆內(nèi)存劃分為另外一部分。前者就稱為Zygote堆,后者就稱為Active堆。以后無(wú)論是Zygote進(jìn)程,還是應(yīng)用程序進(jìn)程,當(dāng)它們需要分配對(duì)象的時(shí)候,都在Active堆上進(jìn)行。這樣就可以使得Zygote堆盡可能少地被執(zhí)行寫操作,因而就可以減少執(zhí)行寫時(shí)拷貝的操作。
在Zygote堆里面分配的對(duì)象其實(shí)主要就是Zygote進(jìn)程在啟動(dòng)過程中預(yù)加載的類、資源和對(duì)象了。這意味著這些預(yù)加載的類、資源和對(duì)象可以在Zygote進(jìn)程和應(yīng)用程序進(jìn)程中做到長(zhǎng)期共享。這樣既能減少拷貝操作,還能減少對(duì)內(nèi)存的需求。?
2. 堆/堆管理 ? ? ?圖2 Dalvik虛擬機(jī)的堆???????
在Dalvik虛擬機(jī)中,堆實(shí)際上就是一塊匿名共享內(nèi)存。Dalvik虛擬機(jī)并不是直接管理這塊匿名共享內(nèi)存,而是將它封裝成一個(gè)mspace,交給C庫(kù)來(lái)管理。mspace是libc中的概念,我們可以通過libc提供的函數(shù)create_mspace_with_base創(chuàng)建一個(gè)mspace,然后再通過mspace_開頭的函數(shù)管理該mspace。例如,我們可以通過mspace_malloc和mspace_bulk_free來(lái)在指定的mspace中分配和釋放內(nèi)存。?實(shí)際上,我們?cè)谑褂胠ibc提供的函數(shù)malloc和free分配和釋放內(nèi)存時(shí),也是在一個(gè)mspace進(jìn)行的,只不過這個(gè)mspace是由libc默認(rèn)創(chuàng)建的。Dalvik虛擬機(jī)除了要給應(yīng)用層分配對(duì)象之外,最重要的還是要對(duì)這些已經(jīng)分配出去的對(duì)象進(jìn)行管理,也就是要在對(duì)象不再被使用的時(shí)候,對(duì)其進(jìn)行自動(dòng)回收。
???? ???? GC回收原理
Dalvik虛擬機(jī)執(zhí)行完成一次垃圾收集之后,我們通常可以看到類似以下的日志輸出:
D/dalvikvm(9050):?GC_CONCURRENT?freed?2049K,?65%?free?3571K/9991K,?external?4703K/5261K,?paused?2ms+2ms ?copy
在這一行日志中, 1、 GC_CONCURRENT表示GC原因 2、 2049K表示總共回收的內(nèi)存 3、 3571K/9991K表示Java Object Heap統(tǒng)計(jì),即在9991K的Java Object Heap中,有3571K是正在使用的 4、 4703K/5261K表示External Memory統(tǒng)計(jì),即在5261K的External Memory中,有4703K是正在使用的5、 2ms+2ms表示垃圾收集造成的程序中止時(shí)間。
Dalivk垃圾收集的使用的?耳熟能詳,大名鼎鼎的的Mark-Sweep算法。?Mark-Sweep垃圾收集算法主要分為兩個(gè)階段:Mark和Sweep。
1.Mark階段從對(duì)象的根集開始標(biāo)記被引用的對(duì)象。標(biāo)記完成后,就進(jìn)入到Sweep階段
2.Sweep階段所做的事情就是回收沒有被標(biāo)記的對(duì)象占用的內(nèi)存。
下面我們來(lái)介紹一下在Mark和Sweep 過程中 堆管理的結(jié)構(gòu)體的作用
1.Bitmap:
這里涉及到的一個(gè)核心概念就是我們?cè)趺礃?biāo)記對(duì)象有沒有被引用的,換句說(shuō)就是通過什么數(shù)據(jù)結(jié)構(gòu)來(lái)描述對(duì)象有沒有被引用。是的,就是圖1中的Heap Bitmap了。Heap Bitmap的結(jié)構(gòu)如圖3所示:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖3 Heap Bitmap
1、在Dalvik虛擬機(jī)中,使用一個(gè)unsigned long數(shù)組來(lái)描述一個(gè)Heap Bitmap。? 2、我們使用libc提供的函數(shù)mspace_malloc來(lái)從堆里面分配內(nèi)存時(shí),得到的內(nèi)存的地址總是對(duì)齊到HB_OBJECT_ALIGNMENT(8)的,也就是說(shuō),我們分配的對(duì)象的地址的最低三位總是0。 為了減少Bitmap的大小。?Bitmap中的位與對(duì)象的對(duì)應(yīng)關(guān)系時(shí),忽略最低三位。
2.Card Table:
在垃圾收集的Mark階段,要求除了垃圾收集線程之外,其它的線程都停止,否則的話,就會(huì)可能導(dǎo)致不能正確地標(biāo)記每一個(gè)對(duì)象。這種現(xiàn)象在垃圾收集算法中稱為Stop The World,會(huì)導(dǎo)致程序中止執(zhí)行,造成停頓的現(xiàn)象。為了盡可能地減少停頓,我們必須要允許在Mark階段有條件地允許程序的其它線程執(zhí)行。這種垃圾收集算法稱為并行垃圾收集算法Concurrent GC。 為了實(shí)現(xiàn)Concurrent GC,Mark階段又劃分兩個(gè)子階段。 1、第一階段:只負(fù)責(zé)標(biāo)記根集對(duì)象。所謂的根集對(duì)象,就是指在GC開始的瞬間,被全局變量、棧變量和寄存器等引用的對(duì)象。 2、第二階段:負(fù)責(zé)標(biāo)記被根集對(duì)象引用的對(duì)象的過程就是。有了這些根集變量之后,我們就可以順著它們找到其余的被引用變量。例如,一個(gè)棧變量引了一個(gè)對(duì)象,而這個(gè)對(duì)象又通過成員變量引用了另外一個(gè)對(duì)象,那該被引用的對(duì)象也會(huì)同時(shí)標(biāo)記為正在使用。
在Concurrent GC,第一個(gè)子階段是不允許垃圾收集線程之外的線程運(yùn)行的,但是第二個(gè)子階段是允許的。不過,在第二個(gè)子階段執(zhí)行的過程中,如果一個(gè)線程修改了一個(gè)對(duì)象,那么該對(duì)象必須要記錄起來(lái),因?yàn)樗苡锌赡芤昧诵碌膶?duì)象,或者引用了之前未引用過的對(duì)象。如果不這樣做的話,那么就會(huì)導(dǎo)致被引用對(duì)象還在使用然而卻被回收。這種情況出現(xiàn)在只進(jìn)行部分垃圾收集的情況,這時(shí)候Card Table的作用就是用來(lái)記錄非垃圾收集堆對(duì)象對(duì)垃圾收集堆對(duì)象的引用。
Dalvik虛擬機(jī)進(jìn)行部分垃圾收集時(shí),實(shí)際上就是只收集在Active堆上分配的對(duì)象。因此對(duì)Dalvik虛擬機(jī)來(lái)說(shuō),Card Table就是用來(lái)記錄在Zygote堆上分配的對(duì)象在部收垃圾收集執(zhí)行過程中對(duì)在Active堆上分配的對(duì)象的引用。 ? ? ? ? 我們是不是想到再用一個(gè)Bitmap在描述上述第二個(gè)子階段被修改的對(duì)象呢?雖然我們盡大努力減少了用來(lái)標(biāo)記對(duì)象的Bitmap的大小,不過還是比較可觀的。
因此,為了減少內(nèi)存的消耗,我們使用另外一種技術(shù)來(lái)標(biāo)記Mark第二子階段被修改的對(duì)象。這種技術(shù)使用到了一種稱為Card Table的數(shù)據(jù)結(jié)構(gòu),如圖4所示:
? ? ? ? ? ? ? ? 圖4 Card Table
??????? 從名字可以看出,Card Table由Card組成,一個(gè)Card實(shí)際上就是一個(gè)字節(jié),它的值要么是CLEAN,要么是DIRTY。
如果一個(gè)Card的值是CLEAN,就表示與它對(duì)應(yīng)的對(duì)象在Mark第二子階段沒有被程序修改過。否則的話,就意味著被程序修改過,對(duì)于這些被修改過的對(duì)象。需要在Mark第二子階段結(jié)束之后,再次禁止垃圾收集線程之外的其它線程執(zhí)行,以便垃圾收集線程再次根據(jù)Card Table記錄的信息對(duì)被修改過的對(duì)象引用的其它對(duì)象進(jìn)行重新標(biāo)記。 由于Mark第二子階段執(zhí)行的時(shí)間不會(huì)太長(zhǎng),因此在該階段被修改的對(duì)象不會(huì)很多,這樣就可以保證第二次子階段結(jié)束后,再次執(zhí)行標(biāo)記對(duì)象的過程是很快的,因而此時(shí)對(duì)程序造成的停頓非常小。
在Card Table中,在連續(xù)GC_CARD_SIZE地址中的對(duì)象共用一個(gè)Card。Dalvik虛擬機(jī)將GC_CARD_SIZE的值設(shè)置為128。因此,假設(shè)堆的大小為Max Heap Size,那么我們只需要一塊字節(jié)數(shù)為(Max Heap Size / 128)的Card Table。相比大小為(Max Heap Size / 8 / 32)× 4的Bitmap,減少了一半的內(nèi)存需求。???? ????
2.Mark Stack: 在Mark階段,Dalvik虛擬機(jī)能過遞歸方式來(lái)標(biāo)記對(duì)象。但是,這不是通過函數(shù)的遞歸調(diào)用來(lái)實(shí)現(xiàn)的,而是借助一個(gè)稱為Mark Stack的棧來(lái)實(shí)現(xiàn)的。
具體來(lái)說(shuō),當(dāng)我們標(biāo)記完成根集對(duì)象之后,就按照它們的地址從小到大的順序標(biāo)記它們所引用的其它對(duì)象。 假設(shè)有A、B、C和D四個(gè)對(duì)象,它的地址大小關(guān)系為A < B < C < D,其中,B和D是根集對(duì)象,A被D引用,C沒有被B和D引用。那么我們將依次遍歷B和D。當(dāng)遍歷到B的時(shí)候,沒有發(fā)現(xiàn)它引用其它對(duì)象,然后就繼續(xù)向前遍歷D對(duì)象。發(fā)現(xiàn)它引用了A對(duì)象。按照遞歸的算法,這時(shí)候除了標(biāo)記A對(duì)象是正在使用之外,還應(yīng)該去檢查A對(duì)象有沒有引用其它對(duì)象,然后又再檢查它引用的對(duì)象有沒有又引用其它的對(duì)象,一直這樣遍歷下去。這樣就跟函數(shù)遞歸一樣。更好的做法是將對(duì)象A記錄在一個(gè)Mark Stack中,然后繼續(xù)檢查地址值比對(duì)象D大的其它對(duì)象。對(duì)于地址值比對(duì)象D大的其它對(duì)象,如果它們引用了一個(gè)地址值比它們小的其它對(duì)象,那么這些其它對(duì)象同樣要記錄在Mark Stack中。等到該輪檢查結(jié)束之后,再回過頭來(lái)檢查記錄在Mark Stack里面的對(duì)象。然后又重復(fù)上述過程,直到Mark Stack等于空為止。 ? ? ? ??? 這就是我們?cè)趫D1中顯示的Mark Stack的作用,它的具體結(jié)構(gòu)如圖5所示:
? ? ? ? ? ? ? ?圖5 Mark Stack
?????? 在Dalvik虛擬機(jī)中,每一個(gè)對(duì)象都是從Object類繼承下來(lái)的,也就是說(shuō),每一個(gè)對(duì)象占用的內(nèi)存大小都至少等于sizeof(Object)。此外,我們通過libc提供的函數(shù)mspace_malloc為對(duì)象分配內(nèi)存時(shí),libc需要額外的內(nèi)存來(lái)記錄被分配出去的內(nèi)存的信息。例如,需要記錄被分配出去的內(nèi)存的大小。每一塊分配出去的內(nèi)存需要額外的HEAP_SOURCE_CHUNK_OVERHEAD內(nèi)存來(lái)記錄上述的管理信息。因此,在Dalvik虛擬機(jī)中,每一個(gè)對(duì)象的大小都至少為sizeof(Object) + HEAP_SOURCE_CHUNK_OVERHEAD。這就意味著對(duì)于一個(gè)大小為Max Heap Size的堆來(lái)說(shuō),最多可以分配Max Heap Size / (sizeof(Object) + HEAP_SOURCE_CHUNK_OVERHEAD)個(gè)對(duì)象。于是,在最壞情況下,我們就需要一個(gè)大小為(Max Heap Size / (sizeof(Object) + HEAP_SOURCE_CHUNK_OVERHEAD))的Object*數(shù)組來(lái)描述Mark Stack,以便可以實(shí)現(xiàn)上述的非遞歸函數(shù)調(diào)用的遞歸標(biāo)記算法。
總結(jié)
以上是生活随笔為你收集整理的Dalivik垃圾回收收机制Cocurrent GC简介的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 魔兽世界60级通灵学院的钥匙任务怎么做
- 下一篇: 魔兽世界怀旧服联盟龙鳞制皮怎么做?联盟龙