? ? ? ?本文簡單介紹了垃圾收集的幾種常見式,重點(diǎn)說明了G1回收的原理(畢竟JDK1.9 G1會(huì)是默認(rèn)的GC回收器–--我們討論的只針對采用HotSpot VM 的openJDK、Oracle
JDK)
?????? 如您已對幾種常見的GC 回收原理有所了解,請您直接跳到最后一章 G1使用。
一、垃圾回收是什么
三種GC 回收方法
在我們了解G1之前,我們先回顧一下JVAV GC的技術(shù)特點(diǎn)。
自動(dòng)內(nèi)存管理,有效的避免了因?yàn)椤巴涐尫拧?,?dǎo)致內(nèi)存無法重復(fù)使用從而產(chǎn)生內(nèi)存泄漏。
現(xiàn)在采用的方法
1. 標(biāo)記-掃描
2. 標(biāo)記-復(fù)制
3. 標(biāo)記-整理
JVM中有一個(gè)非常具體和明確的對象集,稱為垃圾收集根(Garbage Collection Roots)
注:所謂根(Root),就是判斷對象是否可被引用的起始點(diǎn),至于哪里才是根,不同的語言和編譯器都有不同的規(guī)定,但基本是將變量和運(yùn)行??臻g作為根
包含以下內(nèi)容
1 局部變量
2 活動(dòng)線程
3 靜態(tài)字段
4 JNI引用
JVM用于跟蹤所有可到達(dá)(實(shí)時(shí))對象并確保不可訪問對象聲明的內(nèi)存可以重用的方法稱為標(biāo)記和掃描算法。它包括兩個(gè)步驟:
標(biāo)記正在遍歷所有可到達(dá)的對象,從GC根開始并在本機(jī)內(nèi)存中保留有關(guān)所有此類對象的分類帳
掃描確保下一次分配可以重用未引用對象占用的內(nèi)存地址。
JVM中的不同GC算法(例如Parallel Scavenge,Parallel Mark + Copy或CMS)正在以略微不同的方式實(shí)現(xiàn)這些階段,但在概念層面,該過程仍然類似于上述兩個(gè)步驟
關(guān)于這種方法的一個(gè)至關(guān)重要的事情是循環(huán)不再泄漏:

???????????????????? 圖1來源網(wǎng)絡(luò)
但此種標(biāo)記清理回收帶來的問題是,需要暫停應(yīng)用線程以便收集,因?yàn)槿绻绦蛞恢痹谧兓銦o法計(jì)算真正的引用,當(dāng)應(yīng)用程序暫停以便JVM進(jìn)行內(nèi)部引用整理,這種情況就是我們所說的停止世界“stop the wordpause”
注:標(biāo)記和掃描算法在概念上使用最簡單的垃圾處理方法,只需忽略這些對象。這意味著在標(biāo)記階段完成后,未訪問對象占用的所有空間都被視為自由空間,因此可以重用以分配新對象。
這種方法需要使用所謂的自由列表記錄每個(gè)自由區(qū)域及其大小。自由列表的管理增加了對象分配的開銷。這種方法的另一個(gè)弱點(diǎn)是,可能存在大量的空閑區(qū)域,但是如果沒有一個(gè)區(qū)域足夠大以適應(yīng)分配,那么分配仍然會(huì)失?。ㄔ贘ava中有OutOfMeMyRebug錯(cuò)誤)。

當(dāng)進(jìn)行清理時(shí),JVM必須確??梢灾赜锰畛淞藷o法訪問的對象的區(qū)域(圖1中灰色部分)。這可能(并最終會(huì))導(dǎo)致內(nèi)存碎片,與磁盤碎片類似,會(huì)導(dǎo)致兩個(gè)問題:
寫操作變得更加耗時(shí),因?yàn)檎业阶銐虼笮〉南乱粋€(gè)空閑塊不再是一個(gè)簡單的操作。
在創(chuàng)建新對象時(shí),JVM會(huì)在連續(xù)的塊中分配內(nèi)存。因此,如果碎片升級到?jīng)]有單個(gè)空閑片段足以容納新創(chuàng)建的對象的點(diǎn),則會(huì)發(fā)生分配錯(cuò)誤。
為避免此類問題,JVM確保碎片不會(huì)失控。因此,垃圾收集過程中也會(huì)發(fā)生“內(nèi)存碎片整理”過程,而不僅僅是標(biāo)記和掃描。此過程將所有可到達(dá)對象重新定位到彼此旁邊,從而消除(或減少)碎片。這是一個(gè)例子:

注:Mark-Sweep-Compact??算法通過將所有標(biāo)記對象移動(dòng)到存儲(chǔ)區(qū)域的開頭來解決Mark和Sweep的缺點(diǎn)。這種方法的缺點(diǎn)是增加了GC暫停持續(xù)時(shí)間,因?yàn)槲覀冃枰獙⑺袑ο髲?fù)制到新位置并更新對這些對象的所有引用。Mark和Sweep的好處也是可見的 -在這樣的壓縮操作之后,通過指針碰撞,新的對象分配再次非常便宜。使用這種方法,自由空間的位置始終是已知的,并且也不會(huì)觸發(fā)碎片問題。

Jave heap
堆中的內(nèi)存池的基本上是以下劃分的。在不同的GC算法中,某些實(shí)現(xiàn)細(xì)節(jié)可能會(huì)有所不同,但概念仍然有效。

Eden
Eden是內(nèi)存中通常在創(chuàng)建對象時(shí)分配對象的區(qū)域。由于通常有多個(gè)線程同時(shí)創(chuàng)建大量對象,因此Eden進(jìn)一步劃分為?駐留在Eden空間中的一個(gè)或多個(gè)Thread Local Allocation Buffer(簡稱TLAB)。這些緩沖區(qū)允許JVM直接在相應(yīng)的TLAB中分配一個(gè)線程內(nèi)的大多數(shù)對象,從而避免與其他線程的昂貴同步。
當(dāng)無法在TLAB內(nèi)部進(jìn)行分配時(shí)(通常因?yàn)槟抢餂]有足夠的空間),分配將移至共享的Eden空間。如果那里沒有足夠的空間,則會(huì)觸發(fā)Young Generation中的垃圾收集過程以釋放更多空間。如果垃圾收集也沒有在Eden中產(chǎn)生足夠的可用內(nèi)存,那么該對象將在舊代中分配。
收集Eden時(shí),GC會(huì)從根部遍歷所有可到達(dá)的對象,并將它們標(biāo)記為活著。
我們之前已經(jīng)注意到,對象可以具有跨代鏈接,因此直接的方法必須檢查從其他代到Eden的所有引用。不幸的是,這樣做將首先擊敗幾代人。JVM有一個(gè)技巧:卡片標(biāo)記。從本質(zhì)上講,JVM只是標(biāo)記了伊甸園中“臟”物體的粗略位置,這些物體可能與老一代有關(guān)。

標(biāo)記階段完成后,Eden中的所有活動(dòng)對象都將復(fù)制到其中一個(gè)Survivor空間。整個(gè)伊甸園現(xiàn)在被認(rèn)為是空的,可以重復(fù)使用來分配更多的物體。這種方法稱為“標(biāo)記和復(fù)制”:活動(dòng)對象被標(biāo)記,然后被復(fù)制(不移動(dòng))到幸存者空間。
注:標(biāo)記和復(fù)制算法與標(biāo)記和壓縮非常相似,因?yàn)樗鼈円矔?huì)重新定位所有活動(dòng)對象。重要的區(qū)別是,搬遷的目標(biāo)是一個(gè)不同的記憶區(qū)域,作為幸存者的新空間。標(biāo)記和復(fù)制方法有一些優(yōu)點(diǎn),因?yàn)樵谕浑A段復(fù)制可以與標(biāo)記同時(shí)發(fā)生。缺點(diǎn)是還需要一個(gè)足夠大的內(nèi)存區(qū)域來容納幸存的對象。

Survivor
在Eden空間旁邊有兩個(gè)叫做from和to的Survivor spaces(幸存者空間)。重要的是要注意兩個(gè)幸存者空間中的一個(gè)總是空的。
空閑的幸存者空間將在下一次年輕一代被收集時(shí)開始讓對象進(jìn)駐。來自整個(gè)Young一代的所有存在引用連接對象(包括Eden空間和來自'Survivor空間的非空')被復(fù)制到'to'幸存者空間。完成此過程后,'to'現(xiàn)在包含對象,'from'則不包含對象。他們的角色此時(shí)已切換。

在兩個(gè)幸存者空間之間復(fù)制活動(dòng)對象的過程重復(fù)幾次,直到一些對象被認(rèn)為已經(jīng)成熟并且“足夠老”?;诖H假設(shè)(即對象存在年輕代與年老代),預(yù)期存活一段時(shí)間的物體將繼續(xù)使用很長時(shí)間。
因此,這種“終身”對象可以被提升為??老年代。發(fā)生這種情況時(shí),對象不會(huì)從一個(gè)幸存者空間移動(dòng)到另一個(gè)幸存者空間,而是移動(dòng)到舊空間,在那里它們將駐留直到它們變得無法訪問。
為了確定對象是否“足夠老”以便被認(rèn)可可以轉(zhuǎn)移到老年代空間,GC跟蹤特定對象幸存的集合數(shù)。在使用GC完成每一代對象之后,那些仍然存活的對象的年齡會(huì)增加。每當(dāng)年齡超過某個(gè)??期限閾值時(shí),??該對象將被提升為舊空間。
實(shí)際的終身閾值由JVM動(dòng)態(tài)調(diào)整,
-XX:+?MaxTenuringThreshold會(huì)設(shè)置它的上限。
設(shè)置-XX:+
MaxTenuringThreshold = 0會(huì)??導(dǎo)致立即升級,而不會(huì)在Survivor空間之間復(fù)制它。默認(rèn)情況下,現(xiàn)代JVM上的此閾值設(shè)置為15個(gè)GC周期。這也是HotSpot中的最大值。
如果幸存者空間的大小不足以容納Young一代中的所有活物,也可能過早地進(jìn)行升級(Survivor空間復(fù)制老年代 )。
注:
-XX:MaxTenuringThreshold?垃圾最大年齡
如果設(shè)置為0的話,則年輕代對象不經(jīng)過Survivor區(qū),直接進(jìn)入年老代. 對于年老代比較多的應(yīng)用,可以提高效率.如果將此值設(shè)置為一個(gè)較大值,則年輕代對象會(huì)在Survivor區(qū)進(jìn)行多次復(fù)制,這樣可以增加對象再年輕代的存活 時(shí)間,增加在年輕代即被回收的概率
該參數(shù)只有在串行GC時(shí)才有效.
Old Generation
老年代內(nèi)存空間的實(shí)現(xiàn)要復(fù)雜得多。老一代通常要大得多,并且被不太可能是垃圾的物體所占據(jù)。
老一代的GC發(fā)生頻率低于年輕一代。此外,由于預(yù)期大多數(shù)對象在舊一代中都存在,因此不會(huì)發(fā)生標(biāo)記和復(fù)制。相反,移動(dòng)對象以最小化碎片。清理舊空間的算法通常建立在不同的基礎(chǔ)上。清理原理如下:
設(shè)置GC根可訪問的所有對象旁邊的標(biāo)記位來標(biāo)記可到達(dá)的對象
刪除所有無法訪問的對象
]通過將活動(dòng)對象連續(xù)復(fù)制到舊空間的開頭來壓縮舊空間的內(nèi)容
從描述中可以看出,老年代的GC必須處理顯式壓縮以避免過多的碎片
PermGen
Java 8之前,存在一個(gè)稱為“Permanent Generation”永久代的特殊空間。?存放元數(shù)據(jù)(metadata),class信息,內(nèi)部化的字符串(internalized strings),容易造成內(nèi)存泄漏。因?yàn)楹茈y預(yù)測所有這些都需要多少空間。這些失敗預(yù)測的結(jié)果采用java.lang.OutOfMemoryError:Permgen空間的形式。除非此類OutOfMemoryError的原因是實(shí)際的內(nèi)存泄漏,解決此問題的方法是簡單地增加permgen大小,類似于以下示例將允許的最大permgen大小設(shè)置為256 MB:
java -XX:MaxPermSize = 256m
Metaspace
由于預(yù)測元數(shù)據(jù)的需求是一項(xiàng)復(fù)雜而不方便的工作,因此在Java 8中刪除了永久代,以支持Metaspace。從Java 8開始,大多數(shù)各種各樣的事情都轉(zhuǎn)移到了Java堆中。
類定義現(xiàn)在被加載到名為Metaspace的東西中。它位于本機(jī)內(nèi)存中,不會(huì)干擾java heap中的對象。默認(rèn)情況下,Metaspace大小僅受Java進(jìn)程可用的本機(jī)內(nèi)存量的限制。這可以避免開發(fā)人員在向應(yīng)用程序中再添加一個(gè)類而導(dǎo)致java.lang.OutOfMemoryError:
Permgen情況。注意,這種看似無限的空間并不意味著沒有成本——讓元空間無法控制地增長,會(huì)導(dǎo)致嚴(yán)重的swap交換,造成分配失敗。
以通過設(shè)置參數(shù)MaxMetaspaceSize 控制Metaspace的增長。
java -XX:MaxMetaspaceSize = 256m
Minor GC vs Major GCvs Full GC
清除堆內(nèi)存中不同部分的垃圾收集事件通常稱為Minor,Major和Full GC事件。通過監(jiān)視應(yīng)用程序的延遲或吞吐量。將GC事件與結(jié)果相關(guān)聯(lián),監(jiān)控否停止了應(yīng)用程序以及多長時(shí)間。
但由于Minor,Major和Full GC這兩個(gè)術(shù)語被廣泛使用且沒有正確的定義,下面我們分別進(jìn)行說明。
Minor GC
從Young空間收集垃圾稱為Minor GC。這個(gè)定義既清晰又統(tǒng)一。但在處理Minor
Garbage Collection事件時(shí),Minor GC存在以下特點(diǎn):
[if !supportLists]1.???[endif]當(dāng)JVM無法為新對象分配空間時(shí),始終會(huì)觸發(fā)Minor GC,例如Eden已滿。因此,分配率越高,次要GC發(fā)生的頻率就越高。
[if !supportLists]2.???[endif]Mion GC事件期間,Tenured Generation(可認(rèn)為老年代)被忽略。從Tenured Generation(老年代)到Y(jié)oung Generation的引用被認(rèn)為是GC的根(GC roots)。在標(biāo)記階段,簡單地忽略了從Young Generation到Tenured Generation(老年代)的引用。
[if !supportLists]3.???[endif]Minor GC會(huì)觸發(fā)暫停,暫停應(yīng)用程序線程。對于大多數(shù)應(yīng)用程序,如果Eden中的大多數(shù)對象可以被視為垃圾并且永遠(yuǎn)不會(huì)被復(fù)制到幸存者/舊空間,則暫停的長度可以忽略不計(jì)。如果情況相反并且大多數(shù)新生兒物品不符合收集條件,則輕微GC暫停開始花費(fèi)相當(dāng)多的時(shí)間。
因此Minor GC即清理Young?Generation。
Major GC and Full GC
注意,這些術(shù)語沒有正式的定義 - 無論是JVM規(guī)范還是垃圾收集研究論文。但是乍一看,關(guān)于Minor GC清理的年輕代空間之上說明這些定義應(yīng)該很容易:
Major?GC清理老年代空間。
Full??GC正在清理整個(gè)jave heap - 包括Young和Old空間。
首先 - 許多Major GC由Minor GC觸發(fā),多數(shù)情況兩個(gè)是串行發(fā)生的。另一方面 - 現(xiàn)代垃圾收集算法(如G1)執(zhí)行部分垃圾清理,因此,再次使用術(shù)語“清潔”只是部分正確。
這使我們更加??關(guān)注GC是否被稱為Major GC 或Full GC,而應(yīng)該集中精力查明GC是否已停止所有應(yīng)用程序線程,或者是否能夠與應(yīng)用程序線程同時(shí)進(jìn)行。
二、GC算法:實(shí)現(xiàn)與選擇
在JVM中找到的特定實(shí)現(xiàn)。首先要認(rèn)識(shí)到的一個(gè)重要方面是,對于大多數(shù)JVM而言,需要兩種不同的GC算法 - 一種用于清潔Young Generation,另一種用于清潔舊一代。
適用于Java 8,對于較舊的Java版本,可用的組合可能略有不同:

年輕代老年代JVM選項(xiàng)
增加的增加的-Xincgc
串行串行-XX:+ UseSerialGC
并行清除串行-XX:+ UseParallelGC -XX:-UseParallelOldGC
并行新串行N / A
串行平行的老N / A
并行清除平行的老-XX:+ UseParallelGC -XX:+ UseParallelOldGC
并行新平行的老N / A
串行CMS-XX:-UseParNewGC -XX:+? UseConcMarkSweepGC
并行清除CMSN / A
并行新CMS-XX:+ UseParNewGC -XX:+ UseConcMarkSweepGC
G1-XX:+ UseG1GC
下面我們分別介紹各自的工作原理(只有知道原理,我們才能知其所以然,才會(huì)真正使用)
適用于Young和Old代的串行GC
適用于Young和Old的并行GC
對于老一代的年輕+并發(fā)標(biāo)記和掃描(CMS)的并行新功能
G1,包括Young和Old世代的收藏
Serial GC (串行GC)
串行GC 集合使用年輕一代的mark
copy和老年代的mark sweep compact。顧名思義,這兩個(gè)收集器都是單線程收集器,無法并行處理任務(wù)。兩個(gè)收集器應(yīng)用暫停,停止所有應(yīng)用程序線程。
因此,Serial GC算法不能利用現(xiàn)主流硬件中常見的多個(gè)CPU內(nèi)核。與可用的核心數(shù)量無關(guān),JVM在垃圾收集期間只使用一個(gè)。
????? JVM啟動(dòng)腳本中指定單個(gè)參數(shù)來為Young和Old
Generation啟用此GC回收。
java -XX:+ UseSerialGC
Parallel GC(并行GC)
Parallel GC垃圾收集器的組合使用了年輕代的Mark Copy和老年代的Mark Sweep Compact。年輕代和老年代收集都會(huì)觸發(fā)stop-the-world事件,停止所有應(yīng)用程序線程以執(zhí)行垃圾收集。兩個(gè)收集器都使用多個(gè)線程運(yùn)行標(biāo)記和復(fù)制/壓縮階段,因此名稱為“parallel”。使用這種方法,可以大大縮短收集時(shí)間。
垃圾收集過程中使用的線程數(shù)可以通過命令行參數(shù)-xx:parallelGCthreads=nnn進(jìn)行配置。默認(rèn)值等于計(jì)算機(jī)中的核心數(shù)。
并行GC的選擇是通過JVM啟動(dòng)腳本中以下任何參數(shù)組合的規(guī)范來完成的
java -XX:+ UseParallelGC -XX:+ UseParallelOldGC?
如果我們的應(yīng)用優(yōu)化的目標(biāo)是提高吞吐量,并行垃圾收集器適用于多核計(jì)算機(jī)。由于更有效地使用系統(tǒng)資源,因此實(shí)現(xiàn)了更高的吞吐量:
在收集過程中,所有核心都在并行清理垃圾,從而縮短暫停時(shí)間
在垃圾收集周期之間,收集者都沒有消耗任何資源
但另一方面,由于集合的所有階段都必須在沒有任何中斷的情況下發(fā)生,因此這些收集器仍然容易受到長時(shí)間暫停的影響,在此期間應(yīng)用程序線程將被停止。因此,如果延遲是您的主要目標(biāo),則不建議用些組合。
Concurrent Mark and
Sweep(CMS)
這個(gè)垃圾收集器集合的官方名稱是“主要是并發(fā)標(biāo)記和清理垃圾收集器”。它采用了年輕代并行stop-the-word標(biāo)記復(fù)制算法和老年代并行標(biāo)記掃描算法。
CMS收集器的設(shè)計(jì)是為了避免在舊一代收集時(shí)長時(shí)間停頓。它通過兩種方式實(shí)現(xiàn)這一點(diǎn)。首先,它不壓縮舊一代,而是使用空閑列表來管理回收的空間。其次,它與應(yīng)用程序同時(shí)在標(biāo)記和掃描階段完成大部分工作。這意味著垃圾收集不會(huì)顯式地停止應(yīng)用程序線程來執(zhí)行這些階段。但是,應(yīng)該注意的是,它仍然與應(yīng)用程序線程競爭CPU時(shí)間。默認(rèn)情況下,此GC算法使用的線程數(shù)等于計(jì)算機(jī)物理核心數(shù)的1/4。
命令行上指定以下選項(xiàng)來選擇CMS垃圾收集器。
java -XX:+ UseConcMarkSweepGC
如果我們的主要目標(biāo)是低延遲,這種組合在多核計(jì)算機(jī)上是一個(gè)不錯(cuò)的選擇。減少單個(gè)GC暫停的持續(xù)時(shí)間,從而使他們感覺應(yīng)用程序響應(yīng)更快。由于大多數(shù)時(shí)候GC至少消耗了一些CPU資源而沒有執(zhí)行應(yīng)用程序的代碼,因此CMS通常比CPU綁定應(yīng)用程序中的并行GC更差。
G1 – Garbage First
G1的一個(gè)關(guān)鍵設(shè)計(jì)目標(biāo)是使由于垃圾收集而導(dǎo)致的stop-the-word暫停的持續(xù)時(shí)間和分布是可預(yù)測和可配置的,實(shí)際上Garbage-First是一個(gè)軟實(shí)時(shí)垃圾收集器,這意味著可以為其設(shè)置特定的性能目標(biāo)??梢栽谌魏谓o定的y毫秒長時(shí)間范圍內(nèi)請求stop-the-word暫停不超過x毫秒,例如在任何給定秒內(nèi)不超過5毫秒。Garbage-First
GC將盡最大努力以高概率實(shí)現(xiàn)這一目標(biāo)(但不確定,這將是難以實(shí)時(shí)的)。
為實(shí)現(xiàn)這一目標(biāo),G1建立了許多見解。首先,堆不必分成連續(xù)的Young和Old代。相反,堆被分成多個(gè)(通常約2048個(gè))較小的堆區(qū)域,可以容納對象。每個(gè)區(qū)域可以是伊甸園區(qū)域,幸存者區(qū)域或舊區(qū)域。所有伊甸園和幸存者地區(qū)的邏輯聯(lián)盟都是年輕一代,所有舊區(qū)域都是老一代:

這允許GC避免一次收集整個(gè)堆,而是逐步地處理問題:一次只考慮一個(gè)區(qū)域的子集,稱為收集集。在每次暫停期間收集所有Young區(qū)域,但也可以包括一些舊區(qū)域:

G1的另一個(gè)新穎之處在于,在并發(fā)階段,它估計(jì)每個(gè)區(qū)域包含的實(shí)時(shí)數(shù)據(jù)量。這用于構(gòu)建集合集:首先收集包含最多垃圾的區(qū)域。因此名稱:垃圾優(yōu)先收集。
啟用G1收集器的情況下運(yùn)行JVM
java -XX:+ UseG1GC
Evacuation Pause:Fully Young
在應(yīng)用程序生命周期的開始階段,G1沒有來自尚未執(zhí)行的并發(fā)階段的任何附加信息,因此它最初在完全年輕模式下運(yùn)行。當(dāng)Young
Generation填滿時(shí),應(yīng)用程序線程被停止,Young區(qū)域內(nèi)的實(shí)時(shí)數(shù)據(jù)被復(fù)制到Survivor區(qū)域,或任何由此成為Survivor的自由區(qū)域。
復(fù)制這些的過程稱為Evacuation(疏散),它的工作方式與我們之前看到的其他年輕代收集器的工作方式非常相似。
Concurrent Marking(并行標(biāo)記)
?????? G1收集器建立在前一節(jié)中的許多CMS概念的基礎(chǔ)上,盡管它在許多方面有所不同,但并發(fā)標(biāo)記的目標(biāo)是非常相似的。g1并發(fā)標(biāo)記使用Snapshot-At-The-Beginning (初始快照)方法來標(biāo)記標(biāo)記循環(huán)開始時(shí)所有活動(dòng)的對象,即使它們同時(shí)變成垃圾。關(guān)于哪些對象是活動(dòng)的信息允許為每個(gè)區(qū)域建立活動(dòng)狀態(tài),以便以后可以有效地選擇收集集。
然后,這些信息用于在舊區(qū)域中執(zhí)行垃圾收集。如果標(biāo)記確定某個(gè)區(qū)域僅包含垃圾,或者在stop-the-word期間,對包含垃圾和活動(dòng)對象的舊區(qū)域,則可以同時(shí)進(jìn)行Evacuation(疏散)。
當(dāng)堆的總占用量足夠大時(shí),就開始進(jìn)行并發(fā)標(biāo)記。默認(rèn)情況下,它是45%,但這可以通過initiatingEapOccupancyPercent JVM選項(xiàng)更改。與CMS一樣,G1中的并發(fā)標(biāo)記包括許多階段,其中一些階段完全并發(fā),而其中一些階段要求停止應(yīng)用程序線程
三、G1使用
HotSpot有這么多的垃圾回收器,,Serial GC、Parallel GC、Concurrent Mark Sweep GC 這三個(gè)GC選擇:
[if !supportLists]1.?????[endif]如果你想要最小化地使用內(nèi)存和并行開銷,請選Serial GC;
[if !supportLists]2.?????[endif]如果你想要最大化應(yīng)用程序的吞吐量,請選Parallel GC;
[if !supportLists]3.?????[endif]如果你想要最小化GC的中斷或停頓時(shí)間,請選CMS GC。
G1 GC基本思想
G1 GC是一個(gè)壓縮收集器,它基于回收最大量的垃圾原理進(jìn)行設(shè)計(jì)。G1 GC利用遞增、并行、獨(dú)占暫停這些屬性,通過拷貝方式完成壓縮目標(biāo)。此外,它也借助并行、多階段并行標(biāo)記這些方式來幫助減少標(biāo)記、重標(biāo)記、清除暫停的停頓時(shí)間,讓停頓時(shí)間最小化是它的設(shè)計(jì)目標(biāo)之一。
G1的第一論文發(fā)表于2004年,在2012年才在jdk1.7u4中可用。oracle官方計(jì)劃在jdk9中將G1變成默認(rèn)的垃圾收集器,以替代CMS。G1回收器擁有獨(dú)特的垃圾回收策略,這和之前提到的回收器截然不同。從分代上看,G1依然屬于分代型垃圾回收器,它會(huì)區(qū)分年輕代和老年代,年輕代依然有Eden區(qū)和Survivor區(qū),但從堆的結(jié)構(gòu)上看,它并不要求整個(gè)Eden區(qū)、年輕代或者老年代在物理上都是連續(xù)。
綜合來說,G1使用了全新的分區(qū)算法,其特點(diǎn)如下所示:
并行性:G1在回收期間,可以有多個(gè)GC線程同時(shí)工作,有效利用多核計(jì)算能力;
并發(fā)性:G1擁有與應(yīng)用程序交替執(zhí)行的能力,部分工作可以和應(yīng)用程序同時(shí)執(zhí)行,因此,一般來說,不會(huì)在整個(gè)回收階段發(fā)生完全阻塞應(yīng)用程序的情況;
分代GC:G1依然是一個(gè)分代收集器,但是和之前的各類回收器不同,它同時(shí)兼顧年輕代和老年代。對比其他回收器,或者工作在年輕代,或者工作在老年代;
空間整理:G1在回收過程中,會(huì)進(jìn)行適當(dāng)?shù)膶ο笠苿?dòng),不像CMS只是簡單地標(biāo)記清理對象。在若干次GC后,CMS必須進(jìn)行一次碎片整理。而G1不同,它每次回收都會(huì)有效地復(fù)制對象,減少空間碎片,進(jìn)而提升內(nèi)部循環(huán)速度。
可預(yù)見性:由于分區(qū)的原因,G1可以只選取部分區(qū)域進(jìn)行內(nèi)存回收,這樣縮小了回收的范圍,因此對于全局停頓情況的發(fā)生也能得到較好的控制。
隨著G1 GC的出現(xiàn),GC從傳統(tǒng)的連續(xù)堆內(nèi)存布局設(shè)計(jì),逐漸走向不連續(xù)內(nèi)存塊,這是通過引入Region概念實(shí)現(xiàn),也就是說,由一堆不連續(xù)的Region組成了堆內(nèi)存。其實(shí)也不能說是不連續(xù)的,只是它從傳統(tǒng)的物理連續(xù)逐漸改變?yōu)檫壿嬌系倪B續(xù),這是通過Region的動(dòng)態(tài)分配方式實(shí)現(xiàn)的,我們可以把一個(gè)Region分配給Eden、Survivor、老年代、大對象區(qū)間、空閑區(qū)間等的任意一個(gè),而不是固定它的作用,因?yàn)樵绞枪潭?,越是呆板?/p>
G1 GC垃圾回收機(jī)制
首先,G1的設(shè)計(jì)原則就是簡單可行的性能調(diào)優(yōu)開發(fā)人員僅僅需要聲明以下參數(shù)即可:
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
其中-XX:+UseG1GC為開啟G1垃圾收集器,-Xmx32g 設(shè)計(jì)堆內(nèi)存的最大內(nèi)存為32G,-XX:MaxGCPauseMillis=200設(shè)置GC的最大暫停時(shí)間為200ms。如果我們需要調(diào)優(yōu),在內(nèi)存大小一定的情況下,我們只需要修改最大暫停時(shí)間即可。其次,G1將新生代,老年代的物理空間劃分取消了。這樣我們再也不用單獨(dú)的空間對每個(gè)代進(jìn)行設(shè)置了,不用擔(dān)心每個(gè)代內(nèi)存是否足夠。

取而代之的是,G1算法將堆劃分為若干個(gè)區(qū)域(Region),它仍然屬于分代收集器。不過,這些區(qū)域的一部分包含新生代,新生代的垃圾收集依然采用暫停所有應(yīng)用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區(qū)域,G1收集器通過將對象從一個(gè)區(qū)域復(fù)制到另外一個(gè)區(qū)域,完成了清理工作。這就意味著,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會(huì)有cms內(nèi)存碎片問題的存在了。?[if !vml]
[endif]在G1中,還有一種特殊的區(qū)域,叫Humongous區(qū)域。 如果一個(gè)對象占用的空間超過了分區(qū)容量50%以上,G1收集器就認(rèn)為這是一個(gè)巨型對象。這些巨型對象,默認(rèn)直接會(huì)被分配在年老代,但是如果它是一個(gè)短期存在的巨型對象,就會(huì)對垃圾收集器造成負(fù)面影響。為了解決這個(gè)問題,G1劃分了一個(gè)Humongous區(qū),它用來專門存放巨型對象。如果一個(gè)H區(qū)裝不下一個(gè)巨型對象,那么G1會(huì)尋找連續(xù)的H分區(qū)來存儲(chǔ)。為了能找到連續(xù)的H區(qū),有時(shí)候不得不啟動(dòng)Full GC。
PS
:在java 8中,持久代也移動(dòng)到了普通的堆內(nèi)存空間中,改為元空間。對象分配策略說起大對象的分配,我們不得不談?wù)剬ο蟮姆峙洳呗?。它分?個(gè)階段:
TLAB(Thread Local Allocation Buffer)
線程本地分配緩沖區(qū)
Eden
區(qū)中分配
Humongous
區(qū)分配
TLAB為線程本地分配緩沖區(qū),它的目的為了使對象盡可能快的分配出來。如果對象在一個(gè)共享的空間中分配,我們需要采用一些同步機(jī)制來管理這些空間內(nèi)的空閑空間指針。在Eden空間中,每一個(gè)線程都有一個(gè)固定的分區(qū)用于分配對象,即一個(gè)TLAB。分配對象時(shí),線程之間不再需要進(jìn)行任何的同步。對TLAB空間中無法分配的對象,JVM會(huì)嘗試在Eden空間中進(jìn)行分配。如果Eden空間無法容納該對象,就只能在老年代中進(jìn)行分配空間。最后,G1提供了兩種GC模式,Young GC和Mixed GC,兩種都是Stop The World(STW)的。下面我們將分別介紹一下這2種模式。三,G1 Young GC
??? Young GC
主要是對Eden區(qū)進(jìn)行GC,它在Eden空間耗盡時(shí)會(huì)被觸發(fā)。在這種情況下,Eden空間的數(shù)據(jù)移動(dòng)到Survivor空間中,如果Survivor空間不夠,Eden空間的部分?jǐn)?shù)據(jù)會(huì)直接晉升到年老代空間。Survivor區(qū)的數(shù)據(jù)移動(dòng)到新的Survivor區(qū)中,也有部分?jǐn)?shù)據(jù)晉升到老年代空間中。最終Eden空間的數(shù)據(jù)為空,GC停止工作,應(yīng)用線程繼續(xù)執(zhí)行。
這時(shí),需要考慮一個(gè)問題,如果僅僅GC 新生代對象,我們?nèi)绾握业剿械母??老年代的所有對象都是根么?那這樣掃描下來會(huì)耗費(fèi)大量的時(shí)間。于是,G1引進(jìn)了RSet的概念。它的全稱是Remembered Set,作用是跟蹤指向某個(gè)heap區(qū)內(nèi)的對象引用。
[if !vml]
[endif]在CMS中,也有RSet的概念,在老年代中有一塊區(qū)域用來記錄指向新生代的引用。這是一種point-out,在進(jìn)行Young GC時(shí),掃描根時(shí),僅僅需要掃描這一塊區(qū)域,而不需要掃描整個(gè)老年代。
但在G1中,并沒有使用point-out,這是由于一個(gè)分區(qū)太小,分區(qū)數(shù)量太多,如果是用point-out的話,會(huì)造成大量的掃描浪費(fèi),有些根本不需要GC的分區(qū)引用也掃描了。于是G1中使用point-in來解決。point-in的意思是哪些分區(qū)引用了當(dāng)前分區(qū)中的對象。這樣,僅僅將這些對象當(dāng)做根來掃描就避免了無效的掃描。由于新生代有多個(gè),那么我們需要在新生代之間記錄引用嗎?這是不必要的,原因在于每次GC時(shí),所有新生代都會(huì)被掃描,所以只需要記錄老年代到新生代之間的引用即可。
需要注意的是,如果引用的對象很多,賦值器需要對每個(gè)引用做處理,賦值器開銷會(huì)很大,為了解決賦值器開銷這個(gè)問題,在G1 中又引入了另外一個(gè)概念,卡表(Card Table)。一個(gè)Card Table將一個(gè)分區(qū)在邏輯上劃分為固定大小的連續(xù)區(qū)域,每個(gè)區(qū)域稱之為卡??ㄍǔ]^小,介于128到512字節(jié)之間。Card
Table通常為字節(jié)數(shù)組,由Card的索引(即數(shù)組下標(biāo))來標(biāo)識(shí)每個(gè)分區(qū)的空間地址。默認(rèn)情況下,每個(gè)卡都未被引用。當(dāng)一個(gè)地址空間被引用時(shí),這個(gè)地址空間對應(yīng)的數(shù)組索引的值被標(biāo)記為”0″,即標(biāo)記為臟被引用,此外RSet也將這個(gè)數(shù)組下標(biāo)記錄下來。一般情況下,這個(gè)RSet其實(shí)是一個(gè)Hash Table,Key是別的Region的起始地址,Value是一個(gè)集合,里面的元素是Card Table的Index。
Young GC
階段:階段1:根掃描靜態(tài)和本地對象被掃描階段2:更新RS處理dirty card隊(duì)列更新RS
階段3:處理RS檢測從年輕代指向年老代的對象階段4:對象拷貝拷貝存活的對象到survivor/old區(qū)域階段5:處理引用隊(duì)列軟引用,弱引用,虛引用處理
四,G1 Mix GC
Mix GC
不僅進(jìn)行正常的新生代垃圾收集,同時(shí)也回收部分后臺(tái)掃描線程標(biāo)記的老年代分區(qū)。它的GC步驟分2步:全局并發(fā)標(biāo)記(global concurrent marking)拷貝存活對象(evacuation)
在進(jìn)行Mix GC之前,會(huì)先進(jìn)行g(shù)lobal concurrent marking(全局并發(fā)標(biāo)記)。 global
concurrent marking的執(zhí)行過程是怎樣的呢?在G1 GC中,它主要是為Mixed GC提供標(biāo)記服務(wù)的,并不是一次GC過程的一個(gè)必須環(huán)節(jié)。global concurrent marking的執(zhí)行過程分為五個(gè)步驟:初始標(biāo)記(initial mark,STW)在此階段,G1 GC 對根進(jìn)行標(biāo)記。該階段與常規(guī)的 (STW) 年輕代垃圾回收密切相關(guān)。根區(qū)域掃描(root region scan)G1 GC 在初始標(biāo)記的存活區(qū)掃描對老年代的引用,并標(biāo)記被引用的對象。該階段與應(yīng)用程序(非 STW)同時(shí)運(yùn)行,并且只有完成該階段后,才能開始下一次 STW 年輕代垃圾回收。并發(fā)標(biāo)記(Concurrent Marking)G1 GC 在整個(gè)堆中查找可訪問的(存活的)對象。該階段與應(yīng)用程序同時(shí)運(yùn)行,可以被 STW 年輕代垃圾回收中斷最終標(biāo)記(Remark,STW)該階段是 STW 回收,幫助完成標(biāo)記周期。G1 GC 清空 SATB 緩沖區(qū),跟蹤未被訪問的存活對象,并執(zhí)行引用處理。清除垃圾(Cleanup,STW)在這個(gè)最后階段,G1 GC 執(zhí)行統(tǒng)計(jì)和 RSet 凈化的STW 操作。在統(tǒng)計(jì)期間,G1 GC 會(huì)識(shí)別完全空閑的區(qū)域和可供進(jìn)行混合垃圾回收的區(qū)域。清理階段在將空白區(qū)域重置并返回到空閑列表時(shí)為部分并發(fā)。
G1調(diào)優(yōu)
CMS最主要解決了pause time,但是會(huì)占用CPU資源,犧牲吞吐量。CMS默認(rèn)啟動(dòng)的回收線程數(shù)是(CPU數(shù)量+3)/ 4,當(dāng)CPU<4個(gè)時(shí),會(huì)影響用戶線程的執(zhí)行。另外一個(gè)缺點(diǎn)就是內(nèi)存碎片的問題了,碎片會(huì)給大對象的內(nèi)存分配造成麻煩,如果老年代的可用的連續(xù)空間也無法分配時(shí),會(huì)觸發(fā)full gc。并且full gc時(shí)如果發(fā)生young gc會(huì)被young gc打斷,執(zhí)行完young gc之后再繼續(xù)執(zhí)行full gc。
-XX:UseConcMarkSweepGC參數(shù)可以開啟CMS,年輕代使用ParNew,老年代使用CMS,同時(shí)Serial Old收集器將作為CMS收集器出現(xiàn)Concurrent Mode Failure失敗后的后備收集器使用。
MaxGCPauseMillis調(diào)優(yōu)
G1GC
的最基本的參數(shù):
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
前面2個(gè)參數(shù)都好理解,后面這個(gè)MaxGCPauseMillis參數(shù)該怎么配置呢?這個(gè)參數(shù)從字面的意思上看,就是允許的GC最大的暫停時(shí)間。G1盡量確保每次GC暫停的時(shí)間都在設(shè)置的MaxGCPauseMillis范圍內(nèi)。 那G1是如何做到最大暫停時(shí)間的呢?這涉及到另一個(gè)概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的區(qū)域集合。
Young GC
:選定所有新生代里的region。通過控制新生代的region個(gè)數(shù)來控制young GC的開銷。
Mixed GC
:選定所有新生代里的region,外加根據(jù)global
concurrent marking統(tǒng)計(jì)得出收集收益高的若干老年代region。在用戶指定的開銷目標(biāo)范圍內(nèi)盡可能選擇收益高的老年代region。
在理解了這些后,我們再設(shè)置最大暫停時(shí)間就好辦了。首先,我們能容忍的最大暫停時(shí)間是有一個(gè)限度的,我們需要在這個(gè)限度范圍內(nèi)設(shè)置。但是應(yīng)該設(shè)置的值是多少呢?我們需要在吞吐量跟MaxGCPauseMillis之間做一個(gè)平衡。如果MaxGCPauseMillis設(shè)置的過小,那么GC就會(huì)頻繁,吞吐量就會(huì)下降。如果MaxGCPauseMillis設(shè)置的過大,應(yīng)用程序暫停時(shí)間就會(huì)變長。G1的默認(rèn)暫停時(shí)間是200毫秒,我們可以從這里入手,調(diào)整合適的時(shí)間。其他調(diào)優(yōu)參數(shù)
-XX:G1HeapRegionSize=n
設(shè)置的 G1 區(qū)域的大小。值是 2 的冪,范圍是 1 MB 到 32 MB 之間。目標(biāo)是根據(jù)最小的 Java 堆大小劃分出約 2048 個(gè)區(qū)域。
-XX:ParallelGCThreads=n
設(shè)置 STW 工作線程數(shù)的值。將 n 的值設(shè)置為邏輯處理器的數(shù)量。n 的值與邏輯處理器的數(shù)量相同,最多為 8。如果邏輯處理器不止八個(gè),則將 n 的值設(shè)置為邏輯處理器數(shù)的5/8 左右。這適用于大多數(shù)情況,除非是較大的 SPARC 系統(tǒng),其中 n 的值可以是邏輯處理器數(shù)的 5/16 左右。
-XX:ConcGCThreads=n
設(shè)置并行標(biāo)記的線程數(shù)。將 n 設(shè)置為并行垃圾回收線程數(shù)(ParallelGCThreads) 的 1/4 左右。
-XX:InitiatingHeapOccupancyPercent=45
設(shè)置觸發(fā)標(biāo)記周期的 Java 堆占用率閾值。默認(rèn)占用率是整個(gè)Java 堆的 45%。避免使用以下參數(shù):避免使用 -Xmn 選項(xiàng)或 -XX:NewRatio 等其他相關(guān)選項(xiàng)顯式設(shè)置年輕代大小。固定年輕代的大小會(huì)覆蓋暫停時(shí)間目標(biāo)。觸發(fā)Full GC在某些情況下,G1觸發(fā)了Full GC,這時(shí)G1會(huì)退化使用Serial收集器來完成垃圾的清理工作,它僅僅使用單線程來完成GC工作,GC暫停時(shí)間將達(dá)到秒級別的。整個(gè)應(yīng)用處于假死狀態(tài),不能處理任何請求,我們的程序當(dāng)然不希望看到這些。那么發(fā)生Full GC的情況有哪些呢?并發(fā)模式失敗
G1啟動(dòng)標(biāo)記周期,但在Mix GC之前,老年代就被填滿,這時(shí)候G1會(huì)放棄標(biāo)記周期。這種情形下,需要增加堆大小,或者調(diào)整周期(例如增加線程數(shù)-XX:ConcGCThreads等)。晉升失敗或者疏散失敗
G1在進(jìn)行GC的時(shí)候沒有足夠的內(nèi)存供存活對象或晉升對象使用,由此觸發(fā)了Full GC??梢栽谌罩局锌吹?to-space exhausted)或者(to-space overflow)。解決這種問題的方式是:
a,
增加 -XX:G1ReservePercent 選項(xiàng)的值(并相應(yīng)增加總的堆大?。?,為“目標(biāo)空間”增加預(yù)留內(nèi)存量。
b,
通過減少 -XX:InitiatingHeapOccupancyPercent 提前啟動(dòng)標(biāo)記周期。
c,
也可以通過增加 -XX:ConcGCThreads 選項(xiàng)的值來增加并行標(biāo)記線程的數(shù)目。巨型對象分配失敗
當(dāng)巨型對象找不到合適的空間進(jìn)行分配時(shí),就會(huì)啟動(dòng)Full GC,來釋放空間。這種情況下,應(yīng)該避免分配大量的巨型對象,增加內(nèi)存或者增大-XX:G1HeapRegionSize,使巨型對象不再是巨型對象。