原文地址http://blog.csdn.net/zhshulin/article/details/50583724
java中將內(nèi)存的控制交給JVM來(lái)實(shí)現(xiàn),方便了JAVA程序猿,當(dāng)然犧牲了一部分效率,不過(guò)總體來(lái)看是值得的。那么JVM中是如何設(shè)計(jì)GC的呢,本文從幾個(gè)問(wèn)題入手,然后分析了一下設(shè)計(jì)思路,如果有理解錯(cuò)誤的地方,請(qǐng)批評(píng)指正!主要參考了《深入理解JAVA虛擬機(jī)》這本書(shū),圖是盜來(lái)的,圖的內(nèi)容和書(shū)上一樣。
在JVM的內(nèi)存模型中,堆內(nèi)存是JAVA內(nèi)存區(qū)域中最大的一部分,GC主要就是發(fā)生在堆中,用來(lái)回收那些無(wú)用的對(duì)象。這樣直接就引申出了第一個(gè)問(wèn)題:什么樣的對(duì)象需要被回收?判斷條件是什么?如何判斷?
先談?wù)勈裁磳?duì)象需要被回收,OK,我們自己想一想,肯定是沒(méi)用的對(duì)象需要被回收,對(duì)吧?那么如何判斷哪些對(duì)象還有用,哪些沒(méi)用了呢?一個(gè)對(duì)象被創(chuàng)建,如果被引用了,那這個(gè)對(duì)象肯定是有用的對(duì)吧,如果引用全失效了,那就是沒(méi)用的對(duì)象了,需要被回收?;谶@個(gè)思想,引用計(jì)數(shù)法誕生了。引用計(jì)數(shù)算法:這個(gè)非常容易理解,給每個(gè)對(duì)象添加一個(gè)引用計(jì)數(shù)器,對(duì)象每被引用一次,引用計(jì)數(shù)器就+1,引用失效時(shí)就-1。那么判斷一個(gè)對(duì)象是否有用的條件就變成了對(duì)這個(gè)計(jì)數(shù)器值得判斷了,如果為0,那么被回收,如果為>0,那么保留。但是這種方式會(huì)產(chǎn)生一個(gè)問(wèn)題,就是對(duì)象之間的循環(huán)引用無(wú)法被識(shí)別,即使這兩個(gè)對(duì)象不能被訪問(wèn),但是它們之間互相引用著對(duì)方,故而計(jì)數(shù)器肯定>0,那么就不能被回收。JVM中并沒(méi)有使用引用計(jì)數(shù)算法,而是使用了根搜索算法。根搜索算法:這個(gè)算法也不難理解,通過(guò)條件,選擇一系列的對(duì)象成為“GC Roots"對(duì)象,然后將”GC Roots"對(duì)象作為起始點(diǎn)開(kāi)始向下搜索,搜索所有走過(guò)的路徑成為“引用鏈”。在這個(gè)引用鏈上的對(duì)象就保留,而如果一個(gè)或多個(gè)互相引用的對(duì)象不在這個(gè)引用鏈上,或者說(shuō)對(duì)象到“GC Roots"不可達(dá),那么這些就是無(wú)用的對(duì)象,都需要被回收。
注:Java語(yǔ)言中,可作為GC Roots的對(duì)象包括下面幾種:
1) 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
2) 方法區(qū)中類靜態(tài)屬性引用的對(duì)象
3) 方法區(qū)中常量引用的對(duì)象
4) 本地方法棧中JNI(即一般說(shuō)的Native方法)引用的對(duì)象
既然根搜索算法需要考慮到對(duì)象之間的引用,那么就要說(shuō)一下JAVA中對(duì)象的引用類型了:從JDK1.2之后,Java對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用,軟引用,弱引用,虛引用,這四種引用的強(qiáng)度依次減弱
1) 強(qiáng)引用就是指在程序代碼之中普遍存在的,類似 “Object obj = new Object()” 這類的引用,只要強(qiáng)引用還存在,垃圾回收器永遠(yuǎn)不會(huì)回收被引用的對(duì)象。我們也正是利用這個(gè)原理來(lái)重現(xiàn)了OOM異常。
2) 軟引用(SoftReference類)是用來(lái)描述一些還有用但并非需要的對(duì)象,對(duì)于軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi)存異常之前,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收,如果這次回收還沒(méi)有足夠的內(nèi)存,才會(huì)拋出內(nèi)存異常
3) 弱引用(WeakReference類)也是用來(lái)描述非必需對(duì)象的,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次GC發(fā)生之前,當(dāng)垃圾收集器工作時(shí),無(wú)論當(dāng)前內(nèi)存釋放足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象
4) 虛引用(PhantomReference類)也稱為幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系,一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例,對(duì)一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知
那么上述內(nèi)容看完之后想必都知道了什么樣的對(duì)象會(huì)被GC了吧,那么JVM又是通過(guò)什么方式來(lái)回收這些內(nèi)存的呢?下面就需要了解一下垃圾的回收算法了。
標(biāo)記-清除算法 試著想一想,如果要你要設(shè)計(jì)一個(gè)算法清除滿足收集條件的對(duì)象來(lái)釋放內(nèi)存的時(shí)候你該怎么做呢?最簡(jiǎn)單的是不是就是把需要回收的對(duì)象標(biāo)記一下,然后直接全部回收就行了?照著這個(gè)思路就是”標(biāo)記-清除算法”的思想了,算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象。想法很簡(jiǎn)單,實(shí)際也就是這么做的。但是呢,這種方式是不是最好的?有什么缺陷? 想到這里,就需要分析一下了。一個(gè)個(gè)的標(biāo)記然后清除,效率高嗎?當(dāng)然不??纯聪聢D的標(biāo)記-清除算法的示意圖,可以發(fā)現(xiàn),標(biāo)記-清除之后會(huì)產(chǎn)生大量的內(nèi)存碎片,如果碎片太多,當(dāng)程序運(yùn)行沒(méi)有足夠連續(xù)的內(nèi)存空間來(lái)存放大對(duì)象的時(shí)候,就會(huì)不得不提前觸發(fā)一次GC。概括來(lái)說(shuō)就是有兩個(gè)缺點(diǎn):效率不高;內(nèi)存碎片可能導(dǎo)致提前發(fā)生GC。 學(xué)習(xí)算法的童鞋應(yīng)該都很清楚,效率是很重要的,有時(shí)候需要使用空間來(lái)?yè)Q時(shí)間提高效率,那么就需要了解一下第二種回收算法了——復(fù)制算法。
復(fù)制算法 復(fù)制算法呢?它的思想就是空間換時(shí)間,將內(nèi)存容量劃分成相等的兩塊,當(dāng)這一塊的內(nèi)存用完了,就將還存活的內(nèi)存復(fù)制到另一塊上,然后再把使用過(guò)的內(nèi)存空間一次性清理干凈。這樣每次都是對(duì)其中的一塊的內(nèi)存進(jìn)行回收,也就不需要考慮內(nèi)存碎片等復(fù)雜情況了,只需要移動(dòng)堆頂指針,然后按照順序分配即可,實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。但是缺點(diǎn)也很明顯:內(nèi)存變成一半了.......下圖就是復(fù)制算法的示意圖:
我們知道,在JVM中堆內(nèi)存的新生代(new )中的對(duì)象存活率較低,采用復(fù)制算法每次需要復(fù)制的對(duì)象也不是很多,效率較高,空間換時(shí)間值得的?,F(xiàn)在的商業(yè)虛擬機(jī)都是采用復(fù)制算法來(lái)回收新生代,IBM的專門(mén)研究表明:新生代中對(duì)象98%是朝生夕死,所以并不需要按照1:1的比例來(lái)劃分空間來(lái)實(shí)現(xiàn)復(fù)制算法,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中一個(gè)Survivor空間。當(dāng)發(fā)生GC的時(shí)候,將Eden空間和Survivor空間中還存活的對(duì)象拷貝到另一個(gè)沒(méi)使用的Survivor空間中,然后再清理掉Eden和剛剛使用的Survivor空間。Hotspot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1,也就是新生代每次可以使用的內(nèi)存空間是整個(gè)新生代的90%,只有10%的空間會(huì)被浪費(fèi)。 OK,通過(guò)上述的分析,我們知道了在JVM中對(duì)于新生帶的垃圾回收使用的復(fù)制算法(此時(shí)發(fā)生的GC成為young gc),效率高,我們也就只犧牲了10%的內(nèi)存空間,挺不錯(cuò)的。請(qǐng)注意這里提到的young gc,后面會(huì)提到full gc。但是雖然IBM研究表明一般情況下有98%的對(duì)象是朝生夕死,需要回收的,但是不能保證每次回收的時(shí)候?qū)ο蟮拇婊盥识嫉陀?0%啊,是不是?一旦超過(guò)了10%,那么空閑的survivor空間就不夠用了,此時(shí)就必須依賴?yán)夏甏目臻g來(lái)進(jìn)行分配擔(dān)保(就相當(dāng)于A找B借錢(qián),C替A做擔(dān)保,保證如果A換不起就自己來(lái)還,C就是擔(dān)保人,映射到內(nèi)存中老年代所占內(nèi)存就是擔(dān)保人)。如果空閑的Survivor空間無(wú)法存放上次GC之后的存活對(duì)象,那么這些對(duì)象就會(huì)通過(guò)分配擔(dān)保機(jī)制進(jìn)入老年代。 老年代呢,里面保存的都是生存周期較長(zhǎng)的對(duì)象(老年代里面的對(duì)象都是經(jīng)過(guò)了新生代,然后多次存活下來(lái)的對(duì)象),而復(fù)制算法在應(yīng)對(duì)這種存活率極高的內(nèi)存區(qū)域的對(duì)象回收時(shí),需要執(zhí)行較多的復(fù)制操作,效率將會(huì)變低。關(guān)鍵的還是如果不想浪費(fèi)50%的空間,那么就需要分配擔(dān)保機(jī)制(參考新生代的設(shè)計(jì)),但是并沒(méi)有額外的空間來(lái)?yè)?dān)保了。所以對(duì)于老年代的特性,有人提出了一種“標(biāo)記-整理算法”,看到這里肯定就想到了前面提到的“標(biāo)記-清除算法“了,OK,這兩個(gè)算法標(biāo)記的過(guò)程都是一樣的,就在于”標(biāo)記-整理算法”不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存,示意圖如下圖所示。 很明顯,這種”標(biāo)記-整理算法“的效率不高,所以如果老年代發(fā)生GC,那么效率也就不高了,并且一旦老年代發(fā)生GC,那么發(fā)生的必然是Full GC ,Full GC 會(huì)同時(shí)對(duì)老年代和新生代進(jìn)行GC操作,順便也會(huì)回收一下perm gen中的內(nèi)存,所以相比較young gc來(lái)說(shuō)很慢,我們?cè)贘VM調(diào)優(yōu)的時(shí)候需要避免JVM頻繁發(fā)生full gc。full gc的速度比young gc要慢10倍。
分代收集算法 通過(guò)上述的分析呢,就知道了對(duì)于堆中的新生代和老年代會(huì)采用不同的垃圾回收算法來(lái)回收“死亡”的對(duì)象,這種分代回收對(duì)象的方法稱為“分代收集算法”。這個(gè)分代收集算法根據(jù)各個(gè)年代的特點(diǎn)采用適當(dāng)?shù)氖占惴āT谛律?,每次GC的時(shí)候都發(fā)現(xiàn)大批的對(duì)象死去,只有少量存活,自然選用復(fù)制算法;而對(duì)于老年代這種存活率高、沒(méi)有額外擔(dān)??臻g的,就必須使用“標(biāo)記-清除算法”或者“標(biāo)記-整理算法“了。 GC設(shè)計(jì)的理論基礎(chǔ)就是這些了,其實(shí)原理還是比較容易理解的。GC的具體實(shí)現(xiàn)就是垃圾收集器,目前尚沒(méi)有一個(gè)垃圾收集器是完美的,需要配合使用。下面插上一副堆內(nèi)存劃分圖。注:本文寫(xiě)的比較片面,如果想更深入了解,推薦這篇博文:http://jbutton.iteye.com/blog/1569746
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注