G1(Garbage-First)垃圾回收器是在jdk7版本開始被引進的,它的特性在於可以儘量的知足用戶對停頓時間的要求同時還保持較高的吞吐。G1的定位是取代CMS,相比CMS,G1可以更有效的避免碎片化,同時可讓用戶指定預期的停頓時間。算法
G1一樣是分代的垃圾回收,可是不一樣的是G1把整個堆分紅了大小相等的塊(稱爲region),每一個region能夠被分配爲不一樣的角色(young、eden、old等等),這意味着不一樣代的內存大小是不固定的,能夠靈活地調整。數據結構
G1會在jvm啓動時肯定region size(最小1M,最大32M),一般G1會盡可能把堆分爲2048個相同大小的region,具體的region size由此計算,也能夠在jvm參數裏顯示指定。不一樣的region會被分配到不一樣的邏輯角色,好比eden/old,同一個邏輯角色的不一樣region也不必定是連續的。併發
G1的運行機制與CMS相似,G1會進行併發的全局標記來判斷對象的存活與否,在標記結束後,G1就能得知哪些region中的垃圾最多,而後就先回收這部分region,這就是G1的名字的由來。G1採用了一個停頓時間預測的模型來儘量知足用戶指定的停頓時間,根據用戶指定的停頓時間來選擇要回收哪些region。jvm
G1在進行垃圾回收時採用的是複製算法,G1會把各個region中殘留的存活對象複製到單獨的region中,這樣在回收過程當中就完成了內存的整理。爲了下降複製過程當中停頓的時間,整個複製過程是並行的,而CMS並不會進行內存整理,ParallelOld則是會直接整理整個堆,顯然會明顯增長停頓時間。oop
發生young gc時,存活的對象被複制到survivor區,若是對象的年齡超過閾值,那麼會把它晉升到old區。整個young gc過程當中是STW的,同時也會從新計算出下一次GC時的eden區和survivor區的大小,計算過程當中也會考慮用戶指定的目標停頓時間。由於region的設計,要調整各個分區的大小實際上很是容易。post
併發標記是G1中的一個重要階段,這個階段包括若干個步驟,經過併發標記來收集各個region的使用狀況等信息,協助達到用戶指定的停頓時間。性能
這一步是和young gc一塊兒順帶着執行的,首先標記出gc roots直接可達的對象,線程
young gc事後,survivor中的對象都被標記爲root region,這時掃描由survivor區直接可達的old區並標記。這一階段必須在新一輪的young gc前執行完畢。若是這時又須要young gc,那麼會等待掃描完成纔會進行。設計
掃描整個堆,標記存活的對象,整個階段是與應用程序並行的,可能被young gc打斷。這個階段下會不斷從掃描棧取出引用,遞歸地掃描整個堆裏的對象圖。每掃描到一個對象就會對其標記,並將其字段壓入掃描棧。重複掃描過程直到掃描棧清空。過程當中還會掃描SATB write barrier所記錄下的引用。指針
在完成併發標記後,每一個Java線程還會有一些剩下的SATB write barrier記錄的引用還沒有處理。這個階段就負責把剩下的引用處理完。同時這個階段也進行弱引用處理(reference processing)。注意這個暫停與CMS的remark有一個本質上的區別,那就是這個暫停只須要掃描SATB buffer,而CMS的remark須要從新掃描mod-union table裏的dirty card外加整個根集合,而此時整個young gen(無論對象死活)都會被看成根集合的一部分,於是CMS remark有可能會很是慢。
這階段會清理各個region,同時更新Rset,若是有空的region就把它釋放掉。
把存活的對象拷貝到新的region。
在經歷了一個完整的標記週期事後,G1會在下一次young gc的時刻轉換成混合gc,混合gc下,G1可能會把一部分old區的region加入Cset中,利用young gc的算法清理一部分old region。當G1回收了足夠多的old region,又會從新回到young gc,直到下一次併發標記週期完成。
G1的目標是要代替CMS,它把整個堆空間劃分紅了不一樣的region來進行管理,使得分配和回收更加靈活。G1的主要活動包括young gc、mixed gc以及併發標記,它會根據用戶指定的目標停頓時間來決定要對哪些內存區域進行回收。
RSet用於記錄指向某個region的引用,每一個region對應一個RSet,這個數據結構裏記錄了哪些其餘region包含了指向這個region的對象的引用。CSet記錄了GC過程當中會被回收的region,CSet中存活的對象在GC過程當中都會被複制到新的空的region。Rset和Cset都是爲了幫助GC而產生的額外的數據結構。
G1的heap與HotSpot VM的其它GC同樣有一個覆蓋整個heap的card table。邏輯上說,G1的RSet是每一個region有一份。這個RSet記錄的是從別的region指向該region的card。因此這是一種「points-into」的Remembered Set。用card table實現的Remembered Set一般是points-out的,也就是說card table要記錄的是從它覆蓋的範圍出發指向別的範圍的指針。以分代式GC的card table爲例,要記錄old -> young的跨代指針,被標記的card是old gen範圍內的。
G1則是在points-out的card table之上再加了一層結構來構成points-into RSet:每一個region會記錄下到底哪些別的region有指向本身的指針,而這些指針分別在哪些card的範圍內。這個RSet實際上是一個hash table,key是別的region的起始地址,value是一個集合,裏面的元素是card table的index。
G1在concurrent mark階段使用了SATB算法來避免對象的漏標記,而SATB是snapshot at the beginning的縮寫。簡單來講,SATB的思路就是認定在GC開始時存活的對象就是存活的,此時整個堆內的全部對象造成一個快照(snapshot);同時認定在GC過程當中新產生的對象也都是存活的,而剩下的不可達的對象則都是垃圾了。
而G1是如何肯定哪些對象是在GC開始後新產生的呢,這依賴兩個指針:prevTAMS和nextTAMS。TAMS是top at mark start的縮寫,這裏就要再介紹一下region的幾個指針了:
在每次concurrent mark開始時,將當前top賦值給nextTAMS,那麼在concurrent mark過程當中,該region上新分配的對象都落在nextTAMS和top之間,G1保證這部分對象都不會被漏標,默認都是存活的。
當concurrent mark結束時,將當前的nextTAMS賦值給prevTAMS,同時根據mark的結果,將[bottom, prevTAMS]之間的對象的存活信息保存爲一個bitmap,後續就能夠經過這個bitmap肯定對應的對象是否存活了。
因爲對象的存活標記是和應用程序併發執行的,應用程序徹底有可能在標記過程當中修改對象的引用,因此爲了不漏標記,G1使用了write barrier。write barrier是指在"對引用類型字段賦值"這個動做先後的一個攔截,能夠在賦值的先後進行額外的工做。在賦值前的部分的write barrier叫作pre-write barrier,在賦值後的則叫作post-write barrier,G1則使用了pre-write barrier。爲了不漏標,G1會在每次引用賦值前把這個引用指向的舊的值也進行遞歸地標記,並默認其爲存活,這樣就不會漏掉任何一個snapshot中的對象了。當這個舊的值實際上再也不是存活對象時,它實際上也就成爲了浮動垃圾,只能留到下一輪垃圾回收了。
能夠看出,上面提到的barrier中的工做實際上都是在應用程序的線程中完成的。爲了儘可能減小write barrier對性能的影響,G1將一部分本來要在barrier裏作的事情挪到別的線程上併發執行。實現這種分離的方式就是經過logging形式的write barrier:應用程序只在barrier裏把要作的事情的信息記(log)到一個隊列裏,而後另外的線程從隊列裏取出信息批量完成剩餘的動做。
以SATB write barrier爲例,每一個Java線程有一個獨立的、定長的SATBMarkQueue,應用程序線程在barrier裏只把old_value壓入該隊列中。一個隊列滿了以後,它就會被加到全局的SATB隊列集合SATBMarkQueueSet裏等待處理,而後給對應的Java線程換一個新的、乾淨的隊列繼續執行下去。
concurrent mark會按期檢查全局SATB隊列集合的大小。當全局集合中隊列數量超過必定閾值後,concurrent marker就會處理集合裏的全部隊列:把隊列裏記錄的每一個oop都標記上,並將其引用字段壓到標記棧(marking stack)上等後面作進一步標記。