Garbage First(g1)垃圾回收器

cms優秀麼?優秀,但是對於日漸龐大的內存,特別是堆內存超過8g之後,stop the world的時間會被無限拉長,cms並不能給與太大幫助,跨代對象的掃描也是問題,更何況final remark是要掃描整個年輕代,這點看是很難接受的,g1是如何解決這些問題,針對這些問題,設計上怎麼去下手,下面就介紹下g1垃圾回收器,先介紹幾個概念:

Region

cms是有嚴格意義上的分區,這種劃分的特點是各代的存儲地址(邏輯地址)是連續的。而g1的內存沒有嚴格意義上內存分區,所有的內存被分爲一個個大小的Region,默認是2048個,如果你的內存是4096m,那麼每個region的大小爲4096/2048=2m,region的內部是一個個的card page(card)組成,類似於計算機的內存分頁,每個card的大小爲512字節(B),也就是2m * 1024 * 1024/512=4096個card。

RSet

cms採用的是point-out(我引用了誰的對象結構),如果我掃描跨代(老年代引用年輕代)的對象,我不得不遍歷老年代的card table,找到所有的dirty card,這種就有點費勁了,而g1採用Rset,這是一種point-into(誰引用了我的對象結構),這樣判斷跨代(老年代引用年輕代)對象時,只需要掃描自己本身的Rset就可以了,下圖表示了RSet、Card和Region的關係:

G1的RSet是在Card Table的基礎上實現的:每個Region會記錄下別的Region有指向自己的指針,並標記這些指針分別在哪些Card的範圍內。 這個RSet其實是一個Hash Table,Key是別的Region的起始地址,Value是一個集合,裏面的元素是Card Table的Index。

CSet

Collection Set(CSet),它記錄了GC要收集的Region集合,集合裏的Region可以是任意年代的。在GC的時候,對於old->young的跨代對象引用,只要掃描對應的CSet中的RSet即可。對於 Young gc,CSet只包含Young Region,對於混合回收,CSets包含Young Region和Old Region。還有一些概念satb等,這些現在就不說了,後面會詳細的說。

g1是一個全代回收算法,但是年輕代的回收和其他收集器沒有本質上的不同,這裏我就不做介紹了,下面就介紹g1的垃圾回收器過程:

global concurrent marking

這個過程和cms類似,老年代需要佔用一定比例的堆空間才能觸發,通過-XX:InitiatingHeapOccupancyPercent指定,默認值45%,也就是老年代佔堆的比例超過45%。如果Mixed GC週期結束後老年代使用率還是超過45%,那麼會再次觸發全局併發標記過程,這樣就會導致頻繁的老年代GC,影響應用吞吐量。同時老年代空間不大,Mixed GC回收的空間肯定是偏少的。可以適當調高IHOP的值,當然如果此值太高,很容易導致年輕代晉升失敗而出發Full GC,所以需要多次調整測試。

1:初始標記(Initial Marking)

初始標記這個過程需要stop the world,這個過程伴隨着一次young gc,這個就是g1相較於cms優化的地方,省去了cms沿着gc root去trace整個年輕代的過程,省去了一部分標記的時間,這個過程的工作不僅如此,還設置stab快照的二個TAMS變量的值(NTAMS和PTAMS),所有在TAMS之上的對象在這個併發週期內會被識別爲隱式存活對象。這個我一會會詳細的說。

2:根分區掃描(root-region-scan)

初始標記過程之後,對象被複制到survivor當中,這個時候我們需要標記整個survivor的對象作爲老年代的根,這點來看和cms區別並不大,這個過程不需要stop the world;但是如果發生新生代的回收,就證明現在堆的參數有問題,需要調優。

3:併發標記階段(concurrent-mark)

這個階段會沿着上個階段標記到的根對象,遍歷整個的老年代,當然這個過程也不例外,仍然是三色標記法,cms用的是寫後屏障,而g1採用的是寫前屏障,在灰色對象刪除白色對象的時候就記錄下白色對象,這個是極具優化性的一個個設計,這樣在final remark的時候我就不用掃描整個年輕代的對象來確定年輕代到老年代的那些對象關係發生了變化,但是有利必然有弊,有一些真正要刪除的對象可能因此而存活了下來,因此可能存在更多的浮動垃圾,只能期望下一次的gc能夠把他們標記上,而整個的這個過程的基礎依賴於stab(Snapshot-at-beginning),對象的標記,有內部的標記和外部的標記,cms採用的是內部的對象頭標記,g1採用的外部的bitmap標記,我們現在看下這個東西如何實現的,又如何起到這個作用。

這個可以說是g1最核心的地方了,需要重點講一下:

A:Initial Marking第一次的初始標記,prevBitMap爲空(第一次即n-1次,上次的肯定沒有),開始啓用nextBitMap(實際上就是第n次,當前這一次),並設置PTAMS(prevTams)和NTAMS(nextTams),這個圖片的遺憾是沒有把concurrent mark的標記列舉出來,可能處於不同的考慮吧,這個過程以A-B表示。

A-B:這個過程在這裏說一下,concurrent mark過程就是沿着root trace,並在nextBitMap上標記這個對象是存活的,而新增加的對象並不在此次標記的範圍內,爲什麼這樣,stab的原則就是新增加的對象默認存活。這個時候隨着Top指針偏移,新增加的對相就是[NTAMS,Top]這段的對象,這段對象本次gc默認存活,[PTAMS,NTAMS]表示第n輪(當前輪的對象),[Bottom,PTAMS]表示n-1輪(上一輪),而[top,...]就是空閒的內存空間。因爲有了nextBitMap這個快照,當對象的引用發生變化的時候,變化的引用就會被放到SATBMarkQueue這個隊列當中去,在remark階段會做處理。

B:remark再次標記也叫最終標記,正是因爲satb的nextBitMap快照形成了SATBMarkQueue,我們不需要再掃描整個的年輕代,cms必須要掃描整個年輕代,我這裏不多說了,可以看我的cms那篇的介紹,這個階段就是處理SATBMarkQueue裏變化的引用。

C:清理階段,這個階段就是清理region中空閒的的分區,放到free list當中,以期再次使用,這個階段就是nextBitMap複製到prevBitMap,NTAMS和PTAMS互換位置,這個階段結束。

D:又開始一次的Initial Marking,設置NTAMS和Top指針重合,啓用nextBitMap。然後重複上面的過程,大家可能有疑問爲什麼會有這個過程,很大的原因是整個階段的標記結束,如果老年代內存比率不是很高,-XX:InitiatingHeapOccupancyPercent指定,默認值45%,就不會進入mixed gc,而是新一輪的標記,這也是爲什麼會有二個bitMap的原因,不能丟掉上一次標記的結果,當然功能不僅僅如此。

4:重新標記階段(remarking)

上面的stab也簡單介紹了這個過程,不過稍微有些簡單,這個階段也是進行最後的標記,也需要stop the world,主要的作用,就是清除掉SATBMarkQueue記錄的的所有引用變化的對象,找到所有未被標記的存活對象,爲下個階段的對象複製和清除做準備。

5:清理階段(Cleanup Phase

這個階段主要的工作:識別出所有空閒的分區、RSet梳理、將不用的類從metaspace中卸載、回收巨型對象等等。這個階段還會根據每個region的垃圾比率進行排序, -XX:G1MixedGCLiveThresholdPercent指定,不同版本默認值不同,有65%和85%,當垃圾比率超過這個值的時候纔會放到CSet當中,怎麼排序?排序的標準是什麼?這個都是依靠stab完成的,準確的說事prevBitmap和nextBitMap來做的統計。

拷貝存活對象(Evacuation,Mixed gc)

這個過程需要stop the world,這個過程也不是一次完成的,他是伴隨年輕代(minor gc)一塊發生,所以叫做mixed gc,同時回收老年代(Cset裏的)和年輕代的對象。每輪Mixed GC回收的Region最大比例:-XX:G1OldCSetRegionThresholdPercent默認10%,也就是每次回收Mixed GC附加的Cset的Region不超過全部Region的10%,最多10%,和一個週期內觸發Mixed GC最大次數:G1MixedGCCountTarget,默認值8,共同使用,也就是8輪是的mixed gc最多回收80%的region,一般不需要額外調整這二個參數,但是這只是理想狀態,-XX:G1HeapWastePercent(Cset內可被回收的垃圾佔整個堆的比例值)指定,默認值5%,如果垃圾的比率提前降到5%,mixed gc就會提前結束。回收的過程就是把一部分region裏的活對象拷貝到空region裏去,然後回收原本的region的空間。

說道這裏基本就結束了,還有最重要的一點,g1作爲一個可調整的回收算法,是如何預測時間的,這裏就需要說一下g1的Pause Prediction Model停頓預測模型,這個模型的代碼:

//  share/vm/gc_implementation/g1/g1CollectorPolicy.hpp
double get_new_prediction(TruncatedSeq* seq) {
    return MAX2(seq->davg() + sigma() * seq->dsd(),
                seq->davg() * confidence_factor(seq->num()));
}

其實我們實際上也不關心這個,我們關係的是怎麼實現的,g1的典型特徵就是內存塊被分爲一個個的region,每個Region的大小是相等的,這就爲我們計算做了保證,可控的範圍包括minor gc,global concurrent mark,mixed gc,每次的上面的操作都會有一個統計值,每次的所耗費的時間,這些都是在不發生fgc的前提下,fgc是沒辦法控制時間的,根據這些時間,爲了更趨近於-XX:MaxGCPauseMillis:暫停時間,默認值200ms,jvm會盡力的調整年輕代Region的多少,爲什麼是年輕代?因爲不管是minor gc還是miexd gc都是stop the world的,最耗時間的就是年輕代的回收了,但是你也不能吧這個值設置太小,時間越小,年輕代就越小,那麼你會頻繁的進行minor gc,對系統反而沒好處,如果你對停頓時間仍舊不滿意,你需要調 G1MixedGCCountTarget和G1OldCSetRegionThresholdPercent來實現你所需要的停頓時間,調優是一個很緩慢的過程,不肯能一下就成功了,至於用不用g1垃圾回收器,取決於你的生產環境,我們大多數都用的cms,因爲內存大多在4g,太大了並不好,g1在4g的表現並不是很突出,當然在elasticsearch這種喫內存的東西上,你想怎麼用就是你自己的選擇了。

Full GC

G1爲分配擔保預留的空間比例:通過-XX:G1ReservePercent指定,默認10%。也就是老年代會預留10%的空間來給新生代的對象晉升,如果mixed gc回收空間的速度很慢,而對象增長的很快,甚至超過了10%這預留的配額,那麼就會觸發Full gc,Full gc由serial old執行,回收年輕代和老年代。

整個的過程可能與下面的圖(hllvm的R大)有些類似:

到這裏也基本結束了,端午三天因爲疫情的原因,寫了三篇關於jvm的帖子,每篇基本上都需要一天的時間,有些東西從別的地方借來的,但是裏面很多都有我自己的理解,gc的知識我大概用了3年左右的時間一點點的認識,源碼也看過一些,只是理解的時候可能片面,希望對於看到這個帖子的人有所幫助,其中的一些東西我理解的可能也存在問題,真的寫的有點累,jvm的知識也到此結束了,還有幾個案例,時間太久了,我拿不出當時的快照了,沒法分析了,下個我是介紹網絡模型和netty還是lucene呢,我真的感覺lucene是我看過最優秀的東西了,徹底改變我的編程三觀,希望我能堅持寫完我所有學習的東西吧,每個東西都是花費了我不少於3個月的時間一點點摸索的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章