通過之前的學(xué)習(xí)被辑,我們知道了JVM會通過可達(dá)性算法來篩選出哪些對象是可回收的,哪些對象是不可回收的敬惦,GCRoots對象是哪些盼理,java的引用類型有哪些以及finlize()方法的作用。同時我們也知道了當(dāng)一個對象在創(chuàng)建的時候是存放在堆內(nèi)存中的新生代里的俄删,那么當(dāng)新生代內(nèi)存滿了后就會觸發(fā)Minor GC宏怔;但是問題是我們?nèi)绾吾槍π律鷥?nèi)存進(jìn)行管理,以及如何進(jìn)行回收這也是一個值得分析和探討的問題畴椰。
這里針對新生代的垃圾回收算法臊诊,叫做復(fù)制算法
3.1復(fù)制算法
我們先來回顧下之前講堆內(nèi)存的結(jié)構(gòu)分配
存儲在JVM中的Java對象可以被劃分為兩類:
? 一類是生命周期較短的瞬時對象,這類對象的創(chuàng)建和消亡都非常迅速斜脂,生命周期短的抓艳,及時回收即可。
? 另外一類對象的生命周期卻非常長帚戳,在某些極端的情況下還能夠與JVM的生命周期保持一致玷或。
Java堆區(qū)進(jìn)一步細(xì)分的話,可以劃分為年輕代(YoungGen)和老年代(oldGen)片任,其中年輕代又可以劃分為Eden空間偏友、Survivor0空間和Survivor1空間(有時也叫做from區(qū)、to區(qū))对供。
這里大家需要去思考位他,為什么JVM會分成年輕代和老年代,以及年輕代里面又為什么要再劃分出三個區(qū)域产场,這樣做的好處是什么鹅髓?
我們先來分析新生代(年輕代)的復(fù)制算法以及所帶來的的優(yōu)劣
1969年Fenichel提出了一種稱為“半?yún)^(qū)復(fù)制”(Semispace Copying) 的垃圾收集算法, 它將可用內(nèi)存按容量劃分為大小相等的兩塊京景, 每次只使用其中的一塊迈勋。當(dāng)這一塊的內(nèi)存用完了, 就將還存活著的對象復(fù)制到另外一塊上面醋粟, 然后再把已使用過的內(nèi)存空間一次清理掉靡菇。
簡單點來說,就是把新生代的內(nèi)存分為兩塊米愿,如下圖所示:
這時比如我們的代碼如下:
<pre data-tool="mdnice編輯器" style="margin: 10px 0px; padding: 0px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">public class Test { public static void main(String[] args) { registUser(); } public static void registUser(){ User user = new User(); } }
</pre>
那么對應(yīng)內(nèi)存中的分配就如下:
那么如果我們假設(shè)我們的程序不停止厦凤,依然在運(yùn)行,這時不停的調(diào)用registUser()方法生產(chǎn)大量的User對象育苟,對應(yīng)棧幀已經(jīng)退出较鼓,沒有指向?qū)?yīng)的對象,那么就會在堆內(nèi)存中產(chǎn)生大量的垃圾對象:
當(dāng)新生代第一塊區(qū)域內(nèi)容已滿违柏,裝不下的時候博烂,就會觸發(fā)Minor GC回收垃圾。
這時漱竖,如果我們僅僅是采用標(biāo)記算法禽篱,標(biāo)記哪些對象是可回收的,哪些對象是不可回收的馍惹,然后針對可回收的內(nèi)容進(jìn)行回收躺率,那么會導(dǎo)致一個不好的后果,就是產(chǎn)生大量的內(nèi)存碎片万矾。
內(nèi)存碎片一般是由于空閑的連續(xù)空間比要申請的空間小悼吱,導(dǎo)致這些小內(nèi)存塊不能被利用。產(chǎn)生內(nèi)存碎片的方法很簡單良狈,舉個例:假設(shè)有一塊一共有100個單位的連續(xù)空閑內(nèi)存空間后添,范圍是099。如果你從中申請一塊內(nèi)存薪丁,如10個單位遇西,那么申請出來的內(nèi)存塊就為09區(qū)間。這時候你繼續(xù)申請一塊內(nèi)存窥突,比如說5個單位大努溃,第二塊得到的內(nèi)存塊就應(yīng)該為1014區(qū)間。如果你把第一塊內(nèi)存塊釋放阻问,然后再申請一塊大于10個單位的內(nèi)存塊梧税,比如說20個單位。因為剛被釋放的內(nèi)存塊不能滿足新的請求称近,所以只能從15開始分配出20個單位的內(nèi)存塊〉诙樱現(xiàn)在整個內(nèi)存空間的狀態(tài)是09空閑,1014被占用刨秆,1524被占用凳谦,2599空閑。其中09就是一個內(nèi)存碎片了衡未。如果1014一直被占用尸执,而以后申請的空間都大于10個單位家凯,那么09就永遠(yuǎn)用不上了,造成內(nèi)存浪費(fèi)如失。如果你每次申請內(nèi)存的大小绊诲,都比前一次釋放的內(nèi)村大小要小,那么就申請就總能成功褪贵。
如果內(nèi)存碎片過多掂之,就會造成大量的內(nèi)存浪費(fèi),隨著回收的次數(shù)越多脆丁,這樣的碎片可能更多更雜亂世舰,因此這樣直接針對一塊內(nèi)容空間回收的做法是不可取的。
因此JVM采用了復(fù)制算法槽卫,我們圖中有一塊一直未使用的空間可以派上用場了跟压。當(dāng)真正發(fā)生垃圾回收的時候,JVM會將第一塊空間中哪些對象是可回收的晒夹,不能回收的進(jìn)行標(biāo)記裆馒,然后將不可回收的對象統(tǒng)統(tǒng)復(fù)制到下面那塊區(qū)域中,并且復(fù)制的時候可以緊湊的排列在一起丐怯,最大化利用內(nèi)存空間:
那么我們可以直接一次性回收掉上面空間的所有垃圾對象喷好,同時有新的對象產(chǎn)生的時候,直接放在下面這塊區(qū)域進(jìn)行存儲即可读跷。 那么這時上面空間就會騰出梗搅,下面空間就月會越來越多:
當(dāng)下面區(qū)域裝滿的時候,同樣按照剛才的邏輯復(fù)制存活對象到上面區(qū)域效览,一次性回收下面區(qū)域內(nèi)存无切。兩塊區(qū)域內(nèi)存就可以一直重復(fù)循環(huán)使用。
復(fù)制算法的缺點
那么復(fù)制算法確實可以解決內(nèi)存碎片的問題丐枉,也使得我們的回收工作更加效率哆键,不過其缺點也是顯而易見的。這種復(fù)制回收算法的代價是將可用內(nèi)存縮小為了原來的一半瘦锹, 空間浪費(fèi)未免太多了一點 籍嘹。
如果我們給新生代內(nèi)存分配一個G的大小,那么兩塊區(qū)域平均分配弯院,各自占512MB內(nèi)存辱士,從始至終就只有一半的內(nèi)存可用,這樣的算法對內(nèi)存的使用效率就太低了听绳!
現(xiàn)在的商用Java虛擬機(jī)大多都優(yōu)先采用了這種收集算法去回收新生代颂碘, IBM公司曾有一項專門研究對新生代“朝生夕滅”的特點做了更量化的詮釋——新生代中的對象有98%熬不過第一輪收集。因此并不需要按照1∶ 1的比例來劃分新生代的內(nèi)存空間椅挣。
在1989年头岔, Andrew Appel針對具備“朝生夕滅”特點的對象塔拳, 提出了一種更優(yōu)化的半?yún)^(qū)復(fù)制分代策略, 現(xiàn)在稱為“Appel式回收”切油。HotSpot虛擬機(jī)的Serial蝙斜、 ParNew等新生代收集器均采用了這種策略來設(shè)計新生代的內(nèi)存布局。Appel式回收的具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間澎胡, 每次分配內(nèi)存只使用Eden和其中一塊Survivor。發(fā)生垃圾搜集時娩鹉, 將Eden和Survivor中仍然存活的對象一次性復(fù)制到另外一塊Survivor空間上攻谁, 然后直接清理掉Eden和已用過的那塊Survivor空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8∶ 1弯予, 也即每次新生代中可用內(nèi)存空間為整個新生代容量的90%(Eden的80%加上一個Survivor的10%) 戚宦, 只有一個Survivor空間, 即10%的新生代是會被“浪費(fèi)”的锈嫩。當(dāng)然受楼, 98%的對象可被回收僅僅是“普通場景”下測得的數(shù)據(jù), 任何人都沒有辦法百分百保證每次回收都只有不多于10%的對象存活呼寸, 因此Appel式回收還有一個充當(dāng)罕見情況的“逃生門”的安全設(shè)計艳汽, 當(dāng)Survivor空間不足以容納一次Minor GC之后存活的對象時, 就需要依賴其他內(nèi)存區(qū)域(實際上大多就是老年代) 進(jìn)行分配擔(dān)保(Handle Promotion) 对雪。
內(nèi)存的分配擔(dān)保好比我們?nèi)ャy行借款河狐, 如果我們信譽(yù)很好, 在98%的情況下都能按時償還瑟捣, 于是銀行可能會默認(rèn)我們下一次也能按時按量地償還貸款馋艺, 只需要有一個擔(dān)保人能保證如果我不能還款時, 可以從他的賬戶扣錢迈套, 那銀行就認(rèn)為沒有什么風(fēng)險了捐祠。內(nèi)存的分配擔(dān)保也一樣, 如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象桑李, 這些對象便將通過分配擔(dān)保機(jī)制直接進(jìn)入老年代踱蛀, 這對虛擬機(jī)來說就是安全的。
小結(jié)
本章節(jié)我們介紹了JVM垃圾回收的算法-標(biāo)記復(fù)制算法芙扎,以及復(fù)制算法的缺點星岗。下一節(jié)我們將繼續(xù)介紹JVM內(nèi)存的分配以及回收策略,比如:對象優(yōu)先在Eden分配戒洼,大對象直接進(jìn)入老年代俏橘,以及長期存活的對象將進(jìn)入老年代,動態(tài)對象的年齡判斷以及空間分配擔(dān)保原則圈浇。