本文摘自《構(gòu)建高性能的大型分布式Java應(yīng)用》一書(shū),Garbage First簡(jiǎn)稱(chēng)G1,它的目標(biāo)是要做到盡量減少GC所導(dǎo)致的應(yīng)用暫停的時(shí)間,讓?xiě)?yīng)用達(dá)到準(zhǔn)實(shí)時(shí)的效果,同時(shí)保持JVM堆空間的利用率,將作為CMS的替代者在JDK 7中閃亮登場(chǎng),其最大的特色在于允許指定在某個(gè)時(shí)間段內(nèi)GC所導(dǎo)致的應(yīng)用暫停的時(shí)間最大為多少,例如在100秒內(nèi)最多允許GC導(dǎo)致的應(yīng)用暫停時(shí)間為1秒,這個(gè)特性對(duì)于準(zhǔn)實(shí)時(shí)響應(yīng)的系統(tǒng)而言非常的吸引人,這樣就再也不用擔(dān)心系統(tǒng)突然會(huì)暫停個(gè)兩三秒了。
G1要做到這樣的效果,也是有前提的,一方面是硬件環(huán)境的要求,必須是多核的CPU以及較大的內(nèi)存(從規(guī)范來(lái)看,512M以上就滿(mǎn)足條件了),另外一方面是需要接受吞吐量的稍微降低,對(duì)于實(shí)時(shí)性要求高的系統(tǒng)而言,這點(diǎn)應(yīng)該是可以接受的。
為了能夠達(dá)到這樣的效果,G1在原有的各種GC策略上進(jìn)行了吸收和改進(jìn),在G1中可以看到增量收集器和CMS的影子,但它不僅僅是吸收原有GC策略的優(yōu)點(diǎn),并在此基礎(chǔ)上做出了很多的改進(jìn),簡(jiǎn)單來(lái)說(shuō),G1吸收了增量GC以及CMS的精髓,將整個(gè)jvm Heap劃分為多個(gè)固定大小的region,掃描時(shí)采用Snapshot-at-the-beginning的并發(fā)marking算法(具體在后面內(nèi)容詳細(xì)解釋?zhuān)?duì)整個(gè)heap中的region進(jìn)行mark,回收時(shí)根據(jù)region中活躍對(duì)象的bytes進(jìn)行排序,首先回收活躍對(duì)象bytes小以及回收耗時(shí)短(預(yù)估出來(lái)的時(shí)間)的region,回收的方法為將此region中的活躍對(duì)象復(fù)制到另外的region中,根據(jù)指定的GC所能占用的時(shí)間來(lái)估算能回收多少region,這點(diǎn)和以前版本的Full GC時(shí)得處理整個(gè)heap非常不同,這樣就做到了能夠盡量短時(shí)間的暫停應(yīng)用,又能回收內(nèi)存,由于這種策略在回收時(shí)首先回收的是垃圾對(duì)象所占空間最多的region,因此稱(chēng)為Garbage First。
看完上面對(duì)于G1策略的簡(jiǎn)短描述,并不能清楚的掌握G1,在繼續(xù)詳細(xì)看G1的步驟之前,必須先明白G1對(duì)于JVM Heap的改造,這些對(duì)于習(xí)慣了劃分為new generation、old generation的大家來(lái)說(shuō)都有不少的新意。
G1將Heap劃分為多個(gè)固定大小的region,這也是G1能夠?qū)崿F(xiàn)控制GC導(dǎo)致的應(yīng)用暫停時(shí)間的前提,region之間的對(duì)象引用通過(guò)remembered set來(lái)維護(hù),每個(gè)region都有一個(gè)remembered set,remembered set中包含了引用當(dāng)前region中對(duì)象的region的對(duì)象的pointer,由于同時(shí)應(yīng)用也會(huì)造成這些region中對(duì)象的引用關(guān)系不斷的發(fā)生改變,G1采用了Card Table來(lái)用于應(yīng)用通知region修改remembered sets,Card Table由多個(gè)512字節(jié)的Card構(gòu)成,這些Card在Card Table中以1個(gè)字節(jié)來(lái)標(biāo)識(shí),每個(gè)應(yīng)用的線(xiàn)程都有一個(gè)關(guān)聯(lián)的remembered set log,用于緩存和順序化線(xiàn)程運(yùn)行時(shí)造成的對(duì)于card的修改,另外,還有一個(gè)全局的filled RS buffers,當(dāng)應(yīng)用線(xiàn)程執(zhí)行時(shí)修改了card后,如果造成的改變僅為同一region中的對(duì)象之間的關(guān)聯(lián),則不記錄remembered set log,如造成的改變?yōu)榭?/span>region中的對(duì)象的關(guān)聯(lián),則記錄到線(xiàn)程的remembered set log,如線(xiàn)程的remembered set log滿(mǎn)了,則放入全局的filled RS buffers中,線(xiàn)程自身則重新創(chuàng)建一個(gè)新的remembered set log,remembered set本身也是一個(gè)由一堆cards構(gòu)成的哈希表。
盡管G1將Heap劃分為了多個(gè)region,但其默認(rèn)采用的仍然是分代的方式,只是僅簡(jiǎn)單的劃分為了年輕代(young)和非年輕代,這也是由于G1仍然堅(jiān)信大多數(shù)新創(chuàng)建的對(duì)象都是不需要長(zhǎng)的生命周期的,對(duì)于應(yīng)用新創(chuàng)建的對(duì)象,G1將其放入標(biāo)識(shí)為young的region中,對(duì)于這些region,并不記錄remembered set logs,掃描時(shí)只需掃描活躍的對(duì)象,G1在分代的方式上還可更細(xì)的劃分為:fully young或partially young,fully young方式暫停的時(shí)候僅處理young regions,partially同樣處理所有的young regions,但它還會(huì)根據(jù)允許的GC的暫停時(shí)間來(lái)決定是否要加入其他的非young regions,G1是運(yùn)行到fully-young方式還是partially young方式,外部是不能決定的,在啟動(dòng)時(shí),G1采用的為fully-young方式,當(dāng)G1完成一次Concurrent Marking后,則切換為partially young方式,隨后G1跟蹤每次回收的效率,如果回收fully-young中的regions已經(jīng)可以滿(mǎn)足內(nèi)存需要的話(huà),那么就切換回fully young方式,但當(dāng)heap size的大小接近滿(mǎn)的情況下,G1會(huì)切換到partially young方式,以保證能提供足夠的內(nèi)存空間給應(yīng)用使用。
除了分代方式的劃分外,G1還支持另外一種pure
G1的方式,也就是不進(jìn)行代的劃分,pure方式和分代方式的具體不同在下面的具體執(zhí)行步驟中進(jìn)行描述。
掌握了這些概念后,繼續(xù)來(lái)看G1的具體執(zhí)行步驟:
1.
Initial Marking
G1對(duì)于每個(gè)region都保存了兩個(gè)標(biāo)識(shí)用的bitmap,一個(gè)為previous marking bitmap,一個(gè)為next marking bitmap,bitmap中包含了一個(gè)bit的地址信息來(lái)指向?qū)ο蟮钠鹗键c(diǎn)。
開(kāi)始Initial
Marking之前,首先并發(fā)的清空next marking bitmap,然后停止所有應(yīng)用線(xiàn)程,并掃描標(biāo)識(shí)出每個(gè)region中root可直接訪(fǎng)問(wèn)到的對(duì)象,將region中top的值放入next top at mark start(TAMS)中,之后恢復(fù)所有應(yīng)用線(xiàn)程。
觸發(fā)這個(gè)步驟執(zhí)行的條件為:
l G1定義了一個(gè)JVM Heap大小的百分比的閥值,稱(chēng)為h,另外還有一個(gè)H,H的值為(1-h)*Heap Size,目前這個(gè)h的值是固定的,后續(xù)G1也許會(huì)將其改為動(dòng)態(tài)的,根據(jù)jvm的運(yùn)行情況來(lái)動(dòng)態(tài)的調(diào)整,在分代方式下,G1還定義了一個(gè)u以及soft limit,soft limit的值為H-u*Heap Size,當(dāng)Heap中使用的內(nèi)存超過(guò)了soft limit值時(shí),就會(huì)在一次clean up執(zhí)行完畢后在應(yīng)用允許的GC暫停時(shí)間范圍內(nèi)盡快的執(zhí)行此步驟;
l 在pure方式下,G1將marking與clean up組成一個(gè)環(huán),以便clean
up能充分的使用marking的信息,當(dāng)clean up開(kāi)始回收時(shí),首先回收能夠帶來(lái)最多內(nèi)存空間的regions,當(dāng)經(jīng)過(guò)多次的clean up,回收到?jīng)]多少空間的regions時(shí),G1重新初始化一個(gè)新的marking與clean up構(gòu)成的環(huán)。
2.
Concurrent Marking
按照之前Initial
Marking掃描到的對(duì)象進(jìn)行遍歷,以識(shí)別這些對(duì)象的下層對(duì)象的活躍狀態(tài),對(duì)于在此期間應(yīng)用線(xiàn)程并發(fā)修改的對(duì)象的以來(lái)關(guān)系則記錄到remembered set logs中,新創(chuàng)建的對(duì)象則放入比top值更高的地址區(qū)間中,這些新創(chuàng)建的對(duì)象默認(rèn)狀態(tài)即為活躍的,同時(shí)修改top值。
3.
Final Marking Pause
當(dāng)應(yīng)用線(xiàn)程的remembered
set logs未滿(mǎn)時(shí),是不會(huì)放入filled RS buffers中的,在這樣的情況下,這些remebered set logs中記錄的card的修改就會(huì)被更新了,因此需要這一步,這一步要做的就是把應(yīng)用線(xiàn)程中存在的remembered set logs的內(nèi)容進(jìn)行處理,并相應(yīng)的修改remembered sets,這一步需要暫停應(yīng)用,并行的運(yùn)行。
4.
Live Data Counting and Cleanup
值得注意的是,在G1中,并不是說(shuō)Final
Marking Pause執(zhí)行完了,就肯定執(zhí)行Cleanup這步的,由于這步需要暫停應(yīng)用,G1為了能夠達(dá)到準(zhǔn)實(shí)時(shí)的要求,需要根據(jù)用戶(hù)指定的最大的GC造成的暫停時(shí)間來(lái)合理的規(guī)劃什么時(shí)候執(zhí)行Cleanup,另外還有幾種情況也是會(huì)觸發(fā)這個(gè)步驟的執(zhí)行的:
l G1采用的是復(fù)制方法來(lái)進(jìn)行收集,必須保證每次的”to space”的空間都是夠的,因此G1采取的策略是當(dāng)已經(jīng)使用的內(nèi)存空間達(dá)到了H時(shí),就執(zhí)行Cleanup這個(gè)步驟;
l 對(duì)于full-young和partially-young的分代模式的G1而言,則還有情況會(huì)觸發(fā)Cleanup的執(zhí)行,full-young模式下,G1根據(jù)應(yīng)用可接受的暫停時(shí)間、回收young regions需要消耗的時(shí)間來(lái)估算出一個(gè)yound regions的數(shù)量值,當(dāng)JVM中分配對(duì)象的young regions的數(shù)量達(dá)到此值時(shí),Cleanup就會(huì)執(zhí)行;partially-young模式下,則會(huì)盡量頻繁的在應(yīng)用可接受的暫停時(shí)間范圍內(nèi)執(zhí)行Cleanup,并最大限度的去執(zhí)行non-young regions的Cleanup。
這一步中GC線(xiàn)程并行的掃描所有region,計(jì)算每個(gè)region中低于next TAMS值中marked data的大小,然后根據(jù)應(yīng)用所期望的GC的短延時(shí)以及G1對(duì)于region回收所需的耗時(shí)的預(yù)估,排序region,將其中活躍的對(duì)象復(fù)制到其他region中。
G1為了能夠盡量的做到準(zhǔn)實(shí)時(shí)的響應(yīng),例如估算暫停時(shí)間的算法、對(duì)于經(jīng)常被引用的對(duì)象的特殊處理等,G1為了能夠讓GC既能夠充分的回收內(nèi)存,又能夠盡量少的導(dǎo)致應(yīng)用的暫停,可謂費(fèi)盡心思,從G1的論文中的性能評(píng)測(cè)來(lái)看效果也是不錯(cuò)的,不過(guò)如果G1能允許開(kāi)發(fā)人員在編寫(xiě)代碼時(shí)指定哪些對(duì)象是不用mark的就更完美了,這對(duì)于有巨大緩存的應(yīng)用而言,會(huì)有很大的幫助,G1將隨JDK 6 Update 14 beta發(fā)布。