1. G1概覽 算法
G1 GC 全稱是Garbage First Garbage Collector,垃圾優先垃圾回收器,如下簡稱G1。G1是HotSpot JVM的短停頓垃圾回收器。其實關於G1的論文早在2004年就有了,可是G1是在2012年4月發佈的JDK 7u4中才實現。從長期來講,G1旨在取代CMS(Concurrent Mark Sweep)垃圾回收器。G1從JDK9開始已經做爲默認的垃圾回收器。若是對於應用程序來講停頓時間比吞吐量更重要,G1是很是合適的選擇。服務器
整體來講G1具備以下特色:數據結構
2. G1垃圾回收過程多線程
2.1. G1垃圾回收過程概述 併發
G1垃圾回收過程主要包括三個:性能
應用程序分配內存,當年輕代的Eden區用盡時開始年輕代回收過程;當堆內存使用達到必定值(默認45%)時,開始老年代併發標記過程;標記完成立刻開始混合回收過程。spa
舉個例子:我曾經工做的一個Web服務器,Java進程最大堆內存爲4G,每分鐘響應1500個請求,每45秒鐘會新分配大約2G的內存。G1會每45秒鐘進行一次年輕代回收,每31個小時整個堆的使用率會達到45%,會開始老年代併發標記過程,標記完成後開始四到五次的混合回收。線程
下面將會詳細介紹這個三個過程。指針
2.2. G1的內存結構 日誌
理解垃圾回收機制,必須先了解G1的內存結構,內存結構以下圖:
儘管G1堆內存仍然是分代的,可是同一個代的內存再也不採用連續的內存結構。這個是如何實現的呢?
這裏有三個關於內存的概念:代,區和內存分段。
G1把堆內存分爲年輕代和老年代。年輕代分爲Eden和Survivor兩個區,老年代分爲Old和Humongous兩個區。代和區都是邏輯概念。
G1把堆內存分爲大小相等的內存分段,默認狀況下會把內存分爲2048個內存分段,能夠用-XX:G1HeapRegionSize調整內存分段的個數。好比32G堆內存,2048個內存分段每段的大小爲16M。這至關於把內存化整爲零。內存分段是物理概念,表明實際的物理內存空間。
每一個內存分段均可以被標記爲Eden區,Survivor區,Old區,或者Humongous區。這樣屬於不一樣代,不一樣區的內存分段就能夠沒必要是連續內存空間了。
新分配的對象會被分配到Eden區的內存分段上,每一次年輕代的回收過程都會把Eden區存活的對象複製到Survivor區的內存分段上,把Survivor區繼續存活的對象年齡加1,若是Survivor區的存活對象年齡達到某個閾值(好比15,能夠設置),Survivor區的對象會被複制到Old區。複製過程是把源內存分段中全部存活的對象複製到空的目標內存分段上,複製完成後,源內存分段沒有了存活對象,變成了可使用的空的Eden內存分段了;而目標內存分段的對象都是連續存儲的,沒有碎片,因此複製過程能夠達到內存整理的效果,減小碎片。Humongous區用於保存大對象,若是一個對象佔用的空間超過內存分段的一半(好比上面的8M),則此對象將會被分配在Humongous區。若是對象的大小超過一個甚至幾個分段的大小,則對象會分配在物理連續的多個Humongous分段上。Humongous對象由於佔用內存較大而且連續會被優先回收。
2.3. Remembered Set
理解回收過程,須要先了解記憶集合(Remembered Set),如下簡稱RS。爲了在回收單個內存分段的時候沒必要對整個堆內存的對象進行掃描(單個內存分段中的對象可能被其餘內存分段中的對象引用)引入了RS數據結構。RS使得G1能夠在年輕代回收的時候沒必要去掃描老年代的對象,從而提升了性能。每個內存分段都對應一個RS,RS保存了來自其餘分段內的對象對於此分段的引用。對於屬於年輕代的內存分段(Eden和Survivor區的內存分段)來講,RS只保存來自老年代的對象的引用。這是由於年輕代回收是針對所有年輕代的對象的,反正全部年輕代內部的對象引用關係都會被掃描,因此RS不須要保存來自年輕代內部的引用。對於屬於老年代分段的RS來講,也只會保存來自老年代的引用,這是由於老年代的回收以前會先進行年輕代的回收,年輕代回收後Eden區變空了,G1會在老年代回收過程當中掃描Survivor區到老年代的引用。
RS裏的引用信息是怎麼樣填充和維護的呢?簡而言之就是JVM會對應用程序的每個引用賦值語句object.field=object進行記錄和處理,把引用關係更新到RS中。可是這個RS的更新並非實時的。G1維護了一個Dirty Card Queue。對於應用程序的引用賦值語句object.field=object,JVM會在以前和以後執行特殊的操做以在dirty card queue中入隊一個保存了對象引用信息的card。在年輕代回收的時候,G1會對Dirty Card Queue中全部的card進行處理,以更新RS,保證RS實時準確的反映引用關係。那爲何不在引用賦值語句處直接更新RS呢?這是爲了性能的須要,RS的處理須要線程同步,開銷會很大,使用隊列性能會好不少。
2.4. 年輕代回收過程(Young GC)
JVM啓動時,G1先準備好Eden區,程序在運行過程當中不斷建立對象到Eden區,當全部的Eden區都滿了,G1會啓動一次年輕代垃圾回收過程。年輕代只會回收Eden區和Survivor區。首先G1中止應用程序的執行(Stop-The-World),G1建立回收集(Collection Set),回收集是指須要被回收的內存分段的集合,年輕代回收過程的回收集包含年輕代Eden區和Survivor區全部的內存分段。而後開始以下回收過程:
第一階段,掃描根。
根是指static變量指向的對象,正在執行的方法調用鏈條上的局部變量等。根引用連同RS記錄的外部引用做爲掃描存活對象的入口。
第二階段,更新RS。
處理dirty card queue中的card,更新RS。此階段完成後,RS能夠準確的反映老年代對所在的內存分段中對象的引用。
第三階段,處理RS。
識別被老年代對象指向的Eden中的對象,這些被指向的Eden中的對象被認爲是存活的對象。
第四階段,複製對象。
此階段,對象樹被遍歷,Eden區內存段中存活的對象會被複制到Survivor區中空的內存分段,Survivor區內存段中存活的對象若是年齡未達閾值,年齡會加1,達到閥值會被會被複制到Old區中空的內存分段。
第五階段,處理引用。
處理Soft,Weak,Phantom,Final,JNI Weak 等引用。
2.5. G1老年代併發標記過程(Concurrent Marking)
當整個堆內存(包括老年代和新生代)被佔滿必定大小的時候(默認是45%,能夠經過-XX:InitiatingHeapOccupancyPercent進行設置),老年代回收過程會被啓動。具體檢測堆內存使用狀況的時機是年輕代回收以後或者houmongous對象分配以後。老年代回收包含標記老年代內的對象是否存活的過程,標記過程是和應用程序併發運行的(不須要Stop-The-World)。應用程序會改變指針的指向,併發執行的標記過程怎麼能保證標記過程沒有問題呢?併發標記過程有一種情形會對存活的對象標記不到。假設有對象A,B和C,一開始的時候B.c=C,A.c=null。當A的對象樹先被掃描標記,接下來開始掃描B對象樹,此時標記線程被應用程序線程搶佔後停下來,應用程序把A.c=C,B.c=null。當標記線程恢復執行的時候C對象已經標記不到了,這時候C對象實際是存活的,這種情形被稱做對象丟失。G1解決的方法是在對象引用被設置爲空的語句(好比B.c=null)時,把原先指向的對象(C對象)保存到一個隊列,表明它多是存活的。而後會有一個從新標記(Remark)過程處理這些對象,從新標記過程是Stop-The-World的,因此能夠保證標記的正確性。上述這種標記方法被稱爲開始時快照技術(SATB,Snapshot At The Begging)。這種方式會形成某些是垃圾的對象也被當作是存活的,因此G1會使得佔用的內存被實際須要的內存大。
具體標記過程以下:
1. 先進行一次年輕代回收過程,這個過程是Stop-The-World的。
老年代的回收基於年輕代的回收(好比須要年輕代回收過程對於根對象的收集,初始的存活對象的標記)。
2. 恢復應用程序線程的執行。
3. 開始老年代對象的標記過程。
此過程是與應用程序線程併發執行的。標記過程會記錄弱引用狀況,還會計算出每一個分段的對象存活數據(好比分段內存活對象所佔的百分比)。
4. Stop-The-World。
5. 從新標記(Remark)。
此階段從新標記前面提到的STAB隊列中的對象(例子中的C對象),還會處理弱引用。
6. 回收百分之百爲垃圾的內存分段。
注意:不是百分之百爲垃圾的內存分段並不會被處理,這些內存分段中的垃圾是在混合回收過程(Mixed GC)中被回收的。
因爲Humongous對象會獨佔整個內存分段,若是Humongous對象變爲垃圾,則內存分段百分百爲垃圾,因此會在第一時間被回收掉。
7. 恢復應用程序線程的執行。
2.6. 混合回收過程(Mixed GC)
併發標記過程結束之後,緊跟着就會開始混合回收過程。混合回收的意思是年輕代和老年代會同時被回收。併發標記結束之後,老年代中百分百爲垃圾的內存分段被回收了,部分爲垃圾的內存分段被計算了出來。默認狀況下,這些老年代的內存分段會分8次(能夠經過-XX:G1MixedGCCountTarget設置)被回收。混合回收的回收集(Collection Set)包括八分之一的老年代內存分段,Eden區內存分段,Survivor區內存分段。混合回收的算法和年輕代回收的算法徹底同樣,只是回收集多了老年代的內存分段。具體過程請參考上面的年輕代回收過程。
因爲老年代中的內存分段默認分8次回收,G1會優先回收垃圾多的內存分段。垃圾佔內存分段比例越高的,越會被先回收。而且有一個閾值會決定內存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,默認爲65%,意思是垃圾佔內存分段比例要達到65%纔會被回收。若是垃圾佔比過低,意味着存活的對象佔比高,在複製的時候會花費更多的時間。
混合回收並不必定要進行8次。有一個閾值-XX:G1HeapWastePercent,默認值爲10%,意思是容許整個堆內存中有10%的空間被浪費,意味着若是發現能夠回收的垃圾佔堆內存的比例低於10%,則再也不進行混合回收。由於GC會花費不少的時間可是回收到的內存卻不多。
2.7. Full GC
Full GC是指上述方式不能正常工做,G1會中止應用程序的執行(Stop-The-World),使用單線程的內存回收算法進行垃圾回收,性能會很是差,應用程序停頓時間會很長。要避免Full GC的發生,一旦發生須要進行調整。何時回發生Full GC呢?好比堆內存過小,當G1在複製存活對象的時候沒有空的內存分段可用,則會回退到full gc,這種狀況能夠經過增大內存解決。
3. 其餘概念
3.1. 線程本地分配緩衝區(TLAB: Thread Local Allocation Buffer)
因爲堆內存是應用程序共享的,應用程序的多個線程在分配內存的時候須要加鎖以進行同步。爲了不加鎖,提升性能每個應用程序的線程會被分配一個TLAB。TLAB中的內存來自於G1年輕代中的內存分段。當對象不是Humongous對象,TLAB也能裝的下的時候,對象會被優先分配於建立此對象的線程的TLAB中。這樣分配會很快,由於TLAB隸屬於線程,因此不須要加鎖。
3.2. GC「提高」線程本地分配緩衝區(PLAB: Promotion Thread Local Allocation Buffer)
前面提到過,G1會在年輕代回收過程當中把Eden區中的對象複製(「提高」)到Survivor區中,Survivor區中的對象複製到Old區中。G1的回收過程是多線程執行的,爲了不多個線程往同一個內存分段進行復制,那麼複製的過程也須要加鎖。爲了不加鎖,G1的每一個線程都關聯了一個PLAB,這樣就不須要進行加鎖了。
3.3. Remembered Set 粒度
其實RS的存儲分三種粒度,前面提到的Card是最小的一種粒度。粒度的存在是由於某些內存分段中的對象可能很熱門,被來自很是多的區的對象所引用,爲了不保存太多的數據,會以更大的粒度來保存這些引用,好比最大的粒度是用一個bitmap來保存其餘內存分段對RS所對應的內存分段的引用。每個內存分段對應一個bit,若是bit爲0表示該bit對應的內存分段中沒有引用,爲1表示有引用。這種方式會減小RS的數據,可是會增長掃描和標記時的開銷,由於須要掃描全部bit爲1的內存分段中的對象以肯定具體是來自哪一個對象的引用。
後續文章會分析G1 GC的日誌,介紹常見的G1 GC性能問題和經常使用的G1 GC參數調優。
做者公衆號(碼年)掃碼關注:
參考文獻:
G1 Garbage Collector Details and Tuning (Simone Bordet)
Java Performance Companion (Charlie Hunt, Monica Beckwith, Poonam Parhar, Bengt Rutisson