? ? ? ?本文簡單介紹了垃圾收集的幾種常見式,重點說明了G1回收的原理(畢竟JDK1.9 G1會是默認的GC回收器–--我們討論的只針對采用HotSpot VM 的openJDK忽妒、Oracle
JDK)
?????? 如您已對幾種常見的GC 回收原理有所了解苫亦,請您直接跳到最后一章 G1使用俗冻。
一、垃圾回收是什么
三種GC 回收方法
在我們了解G1之前虚婿,我們先回顧一下JVAV GC的技術特點。
自動內存管理泳挥,有效的避免了因為“忘記釋放”然痊,導致內存無法重復使用從而產生內存泄漏。
現(xiàn)在采用的方法
1. 標記-掃描
2. 標記-復制
3. 標記-整理
JVM中有一個非常具體和明確的對象集羡洁,稱為垃圾收集根(Garbage Collection Roots)
注:所謂根(Root),就是判斷對象是否可被引用的起始點爽丹,至于哪里才是根筑煮,不同的語言和編譯器都有不同的規(guī)定,但基本是將變量和運行椩列空間作為根
包含以下內容
1 局部變量
2 活動線程
3 靜態(tài)字段
4 JNI引用
JVM用于跟蹤所有可到達(實時)對象并確保不可訪問對象聲明的內存可以重用的方法稱為標記和掃描算法真仲。它包括兩個步驟:
標記正在遍歷所有可到達的對象,從GC根開始并在本機內存中保留有關所有此類對象的分類帳
掃描確保下一次分配可以重用未引用對象占用的內存地址初澎。
JVM中的不同GC算法(例如Parallel Scavenge秸应,Parallel Mark + Copy或CMS)正在以略微不同的方式實現(xiàn)這些階段,但在概念層面碑宴,該過程仍然類似于上述兩個步驟
關于這種方法的一個至關重要的事情是循環(huán)不再泄漏:
???????????????????? 圖1來源網(wǎng)絡
但此種標記清理回收帶來的問題是软啼,需要暫停應用線程以便收集,因為如果程序一直在變化延柠,便無法計算真正的引用祸挪,當應用程序暫停以便JVM進行內部引用整理,這種情況就是我們所說的停止世界“stop the wordpause”
注:標記和掃描算法在概念上使用最簡單的垃圾處理方法贞间,只需忽略這些對象贿条。這意味著在標記階段完成后,未訪問對象占用的所有空間都被視為自由空間增热,因此可以重用以分配新對象整以。
這種方法需要使用所謂的自由列表記錄每個自由區(qū)域及其大小。自由列表的管理增加了對象分配的開銷峻仇。這種方法的另一個弱點是公黑,可能存在大量的空閑區(qū)域,但是如果沒有一個區(qū)域足夠大以適應分配摄咆,那么分配仍然會失敺鳌(在Java中有OutOfMeMyRebug錯誤)。
當進行清理時豆同,JVM必須確狈可以重用填充了無法訪問的對象的區(qū)域(圖1中灰色部分)。這可能(并最終會)導致內存碎片影锈,與磁盤碎片類似芹务,會導致兩個問題:
寫操作變得更加耗時蝉绷,因為找到足夠大小的下一個空閑塊不再是一個簡單的操作。
在創(chuàng)建新對象時枣抱,JVM會在連續(xù)的塊中分配內存熔吗。因此,如果碎片升級到?jīng)]有單個空閑片段足以容納新創(chuàng)建的對象的點佳晶,則會發(fā)生分配錯誤桅狠。
為避免此類問題,JVM確保碎片不會失控轿秧。因此中跌,垃圾收集過程中也會發(fā)生“內存碎片整理”過程,而不僅僅是標記和掃描菇篡。此過程將所有可到達對象重新定位到彼此旁邊漩符,從而消除(或減少)碎片。這是一個例子:
注:Mark-Sweep-Compact??算法通過將所有標記對象移動到存儲區(qū)域的開頭來解決Mark和Sweep的缺點驱还。這種方法的缺點是增加了GC暫停持續(xù)時間嗜暴,因為我們需要將所有對象復制到新位置并更新對這些對象的所有引用。Mark和Sweep的好處也是可見的 -在這樣的壓縮操作之后议蟆,通過指針碰撞闷沥,新的對象分配再次非常便宜。使用這種方法咐容,自由空間的位置始終是已知的狐赡,并且也不會觸發(fā)碎片問題。
Jave heap
堆中的內存池的基本上是以下劃分的疟丙。在不同的GC算法中颖侄,某些實現(xiàn)細節(jié)可能會有所不同,但概念仍然有效享郊。
Eden
Eden是內存中通常在創(chuàng)建對象時分配對象的區(qū)域览祖。由于通常有多個線程同時創(chuàng)建大量對象,因此Eden進一步劃分為?駐留在Eden空間中的一個或多個Thread Local Allocation Buffer(簡稱TLAB)炊琉。這些緩沖區(qū)允許JVM直接在相應的TLAB中分配一個線程內的大多數(shù)對象展蒂,從而避免與其他線程的昂貴同步。
當無法在TLAB內部進行分配時(通常因為那里沒有足夠的空間)苔咪,分配將移至共享的Eden空間锰悼。如果那里沒有足夠的空間,則會觸發(fā)Young Generation中的垃圾收集過程以釋放更多空間团赏。如果垃圾收集也沒有在Eden中產生足夠的可用內存箕般,那么該對象將在舊代中分配。
收集Eden時舔清,GC會從根部遍歷所有可到達的對象丝里,并將它們標記為活著曲初。
我們之前已經(jīng)注意到,對象可以具有跨代鏈接杯聚,因此直接的方法必須檢查從其他代到Eden的所有引用臼婆。不幸的是,這樣做將首先擊敗幾代人幌绍。JVM有一個技巧:卡片標記颁褂。從本質上講,JVM只是標記了伊甸園中“臟”物體的粗略位置傀广,這些物體可能與老一代有關颁独。
標記階段完成后,Eden中的所有活動對象都將復制到其中一個Survivor空間主儡。整個伊甸園現(xiàn)在被認為是空的奖唯,可以重復使用來分配更多的物體惨缆。這種方法稱為“標記和復制”:活動對象被標記糜值,然后被復制(不移動)到幸存者空間。
注:標記和復制算法與標記和壓縮非常相似坯墨,因為它們也會重新定位所有活動對象寂汇。重要的區(qū)別是,搬遷的目標是一個不同的記憶區(qū)域捣染,作為幸存者的新空間骄瓣。標記和復制方法有一些優(yōu)點,因為在同一階段復制可以與標記同時發(fā)生耍攘。缺點是還需要一個足夠大的內存區(qū)域來容納幸存的對象榕栏。
Survivor
在Eden空間旁邊有兩個叫做from和to的Survivor spaces(幸存者空間)。重要的是要注意兩個幸存者空間中的一個總是空的蕾各。
空閑的幸存者空間將在下一次年輕一代被收集時開始讓對象進駐扒磁。來自整個Young一代的所有存在引用連接對象(包括Eden空間和來自'Survivor空間的非空')被復制到'to'幸存者空間。完成此過程后式曲,'to'現(xiàn)在包含對象妨托,'from'則不包含對象。他們的角色此時已切換吝羞。
在兩個幸存者空間之間復制活動對象的過程重復幾次兰伤,直到一些對象被認為已經(jīng)成熟并且“足夠老”【牛基于代際假設(即對象存在年輕代與年老代)敦腔,預期存活一段時間的物體將繼續(xù)使用很長時間。
因此恨溜,這種“終身”對象可以被提升為??老年代会烙。發(fā)生這種情況時负懦,對象不會從一個幸存者空間移動到另一個幸存者空間,而是移動到舊空間柏腻,在那里它們將駐留直到它們變得無法訪問纸厉。
為了確定對象是否“足夠老”以便被認可可以轉移到老年代空間,GC跟蹤特定對象幸存的集合數(shù)五嫂。在使用GC完成每一代對象之后颗品,那些仍然存活的對象的年齡會增加。每當年齡超過某個??期限閾值時沃缘,??該對象將被提升為舊空間躯枢。
實際的終身閾值由JVM動態(tài)調整,
-XX:+?MaxTenuringThreshold會設置它的上限槐臀。
設置-XX:+
MaxTenuringThreshold = 0會??導致立即升級锄蹂,而不會在Survivor空間之間復制它。默認情況下水慨,現(xiàn)代JVM上的此閾值設置為15個GC周期得糜。這也是HotSpot中的最大值。
如果幸存者空間的大小不足以容納Young一代中的所有活物晰洒,也可能過早地進行升級(Survivor空間復制老年代 )朝抖。
注:
-XX:MaxTenuringThreshold?垃圾最大年齡
如果設置為0的話,則年輕代對象不經(jīng)過Survivor區(qū),直接進入年老代. 對于年老代比較多的應用,可以提高效率.如果將此值設置為一個較大值,則年輕代對象會在Survivor區(qū)進行多次復制,這樣可以增加對象再年輕代的存活 時間,增加在年輕代即被回收的概率
該參數(shù)只有在串行GC時才有效.
Old Generation
老年代內存空間的實現(xiàn)要復雜得多。老一代通常要大得多谍珊,并且被不太可能是垃圾的物體所占據(jù)治宣。
老一代的GC發(fā)生頻率低于年輕一代。此外砌滞,由于預期大多數(shù)對象在舊一代中都存在侮邀,因此不會發(fā)生標記和復制。相反贝润,移動對象以最小化碎片绊茧。清理舊空間的算法通常建立在不同的基礎上。清理原理如下:
設置GC根可訪問的所有對象旁邊的標記位來標記可到達的對象
刪除所有無法訪問的對象
]通過將活動對象連續(xù)復制到舊空間的開頭來壓縮舊空間的內容
從描述中可以看出题暖,老年代的GC必須處理顯式壓縮以避免過多的碎片
PermGen
Java 8之前按傅,存在一個稱為“Permanent Generation”永久代的特殊空間。?存放元數(shù)據(jù)(metadata)胧卤,class信息唯绍,內部化的字符串(internalized strings),容易造成內存泄漏枝誊。因為很難預測所有這些都需要多少空間况芒。這些失敗預測的結果采用java.lang.OutOfMemoryError:Permgen空間的形式。除非此類OutOfMemoryError的原因是實際的內存泄漏叶撒,解決此問題的方法是簡單地增加permgen大小绝骚,類似于以下示例將允許的最大permgen大小設置為256 MB:
java -XX:MaxPermSize = 256m
Metaspace
由于預測元數(shù)據(jù)的需求是一項復雜而不方便的工作耐版,因此在Java 8中刪除了永久代,以支持Metaspace压汪。從Java 8開始粪牲,大多數(shù)各種各樣的事情都轉移到了Java堆中。
類定義現(xiàn)在被加載到名為Metaspace的東西中止剖。它位于本機內存中腺阳,不會干擾java heap中的對象。默認情況下穿香,Metaspace大小僅受Java進程可用的本機內存量的限制亭引。這可以避免開發(fā)人員在向應用程序中再添加一個類而導致java.lang.OutOfMemoryError:
Permgen情況。注意皮获,這種看似無限的空間并不意味著沒有成本——讓元空間無法控制地增長焙蚓,會導致嚴重的swap交換,造成分配失敗洒宝。
以通過設置參數(shù)MaxMetaspaceSize 控制Metaspace的增長购公。
java -XX:MaxMetaspaceSize = 256m
Minor GC vs Major GCvs Full GC
清除堆內存中不同部分的垃圾收集事件通常稱為Minor,Major和Full GC事件待德。通過監(jiān)視應用程序的延遲或吞吐量君丁。將GC事件與結果相關聯(lián)枫夺,監(jiān)控否停止了應用程序以及多長時間将宪。
但由于Minor,Major和Full GC這兩個術語被廣泛使用且沒有正確的定義橡庞,下面我們分別進行說明较坛。
Minor GC
從Young空間收集垃圾稱為Minor GC。這個定義既清晰又統(tǒng)一扒最。但在處理Minor
Garbage Collection事件時丑勤,Minor GC存在以下特點:
[if !supportLists]1.???[endif]當JVM無法為新對象分配空間時,始終會觸發(fā)Minor GC吧趣,例如Eden已滿法竞。因此,分配率越高强挫,次要GC發(fā)生的頻率就越高岔霸。
[if !supportLists]2.???[endif]Mion GC事件期間,Tenured Generation(可認為老年代)被忽略俯渤。從Tenured Generation(老年代)到Young Generation的引用被認為是GC的根(GC roots)呆细。在標記階段,簡單地忽略了從Young Generation到Tenured Generation(老年代)的引用八匠。
[if !supportLists]3.???[endif]Minor GC會觸發(fā)暫停絮爷,暫停應用程序線程趴酣。對于大多數(shù)應用程序,如果Eden中的大多數(shù)對象可以被視為垃圾并且永遠不會被復制到幸存者/舊空間坑夯,則暫停的長度可以忽略不計岖寞。如果情況相反并且大多數(shù)新生兒物品不符合收集條件,則輕微GC暫停開始花費相當多的時間柜蜈。
因此Minor GC即清理Young?Generation慎璧。
Major GC and Full GC
注意,這些術語沒有正式的定義 - 無論是JVM規(guī)范還是垃圾收集研究論文。但是乍一看蹭劈,關于Minor GC清理的年輕代空間之上說明這些定義應該很容易:
Major?GC清理老年代空間措伐。
Full??GC正在清理整個jave heap - 包括Young和Old空間。
首先 - 許多Major GC由Minor GC觸發(fā)岁疼,多數(shù)情況兩個是串行發(fā)生的。另一方面 - 現(xiàn)代垃圾收集算法(如G1)執(zhí)行部分垃圾清理缆娃,因此捷绒,再次使用術語“清潔”只是部分正確。
這使我們更加??關注GC是否被稱為Major GC 或Full GC贯要,而應該集中精力查明GC是否已停止所有應用程序線程暖侨,或者是否能夠與應用程序線程同時進行。
二崇渗、GC算法:實現(xiàn)與選擇
在JVM中找到的特定實現(xiàn)字逗。首先要認識到的一個重要方面是,對于大多數(shù)JVM而言宅广,需要兩種不同的GC算法 - 一種用于清潔Young Generation葫掉,另一種用于清潔舊一代。
適用于Java 8跟狱,對于較舊的Java版本俭厚,可用的組合可能略有不同:
年輕代老年代JVM選項
增加的增加的-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
下面我們分別介紹各自的工作原理(只有知道原理,我們才能知其所以然驶臊,才會真正使用)
適用于Young和Old代的串行GC
適用于Young和Old的并行GC
對于老一代的年輕+并發(fā)標記和掃描(CMS)的并行新功能
G1挪挤,包括Young和Old世代的收藏
Serial GC (串行GC)
串行GC 集合使用年輕一代的mark
copy和老年代的mark sweep compact。顧名思義关翎,這兩個收集器都是單線程收集器扛门,無法并行處理任務。兩個收集器應用暫停笤休,停止所有應用程序線程尖飞。
因此,Serial GC算法不能利用現(xiàn)主流硬件中常見的多個CPU內核。與可用的核心數(shù)量無關政基,JVM在垃圾收集期間只使用一個贞铣。
????? JVM啟動腳本中指定單個參數(shù)來為Young和Old
Generation啟用此GC回收。
java -XX:+ UseSerialGC
Parallel GC(并行GC)
Parallel GC垃圾收集器的組合使用了年輕代的Mark Copy和老年代的Mark Sweep Compact沮明。年輕代和老年代收集都會觸發(fā)stop-the-world事件辕坝,停止所有應用程序線程以執(zhí)行垃圾收集。兩個收集器都使用多個線程運行標記和復制/壓縮階段荐健,因此名稱為“parallel”酱畅。使用這種方法,可以大大縮短收集時間江场。
垃圾收集過程中使用的線程數(shù)可以通過命令行參數(shù)-xx:parallelGCthreads=nnn進行配置纺酸。默認值等于計算機中的核心數(shù)。
并行GC的選擇是通過JVM啟動腳本中以下任何參數(shù)組合的規(guī)范來完成的
java -XX:+ UseParallelGC -XX:+ UseParallelOldGC?
如果我們的應用優(yōu)化的目標是提高吞吐量址否,并行垃圾收集器適用于多核計算機餐蔬。由于更有效地使用系統(tǒng)資源,因此實現(xiàn)了更高的吞吐量:
在收集過程中佑附,所有核心都在并行清理垃圾樊诺,從而縮短暫停時間
在垃圾收集周期之間,收集者都沒有消耗任何資源
但另一方面音同,由于集合的所有階段都必須在沒有任何中斷的情況下發(fā)生词爬,因此這些收集器仍然容易受到長時間暫停的影響,在此期間應用程序線程將被停止权均。因此顿膨,如果延遲是您的主要目標,則不建議用些組合螺句。
Concurrent Mark and
Sweep(CMS)
這個垃圾收集器集合的官方名稱是“主要是并發(fā)標記和清理垃圾收集器”虽惭。它采用了年輕代并行stop-the-word標記復制算法和老年代并行標記掃描算法橡类。
CMS收集器的設計是為了避免在舊一代收集時長時間停頓蛇尚。它通過兩種方式實現(xiàn)這一點。首先顾画,它不壓縮舊一代取劫,而是使用空閑列表來管理回收的空間。其次研侣,它與應用程序同時在標記和掃描階段完成大部分工作谱邪。這意味著垃圾收集不會顯式地停止應用程序線程來執(zhí)行這些階段。但是庶诡,應該注意的是惦银,它仍然與應用程序線程競爭CPU時間。默認情況下,此GC算法使用的線程數(shù)等于計算機物理核心數(shù)的1/4扯俱。
命令行上指定以下選項來選擇CMS垃圾收集器书蚪。
java -XX:+ UseConcMarkSweepGC
如果我們的主要目標是低延遲,這種組合在多核計算機上是一個不錯的選擇迅栅。減少單個GC暫停的持續(xù)時間殊校,從而使他們感覺應用程序響應更快。由于大多數(shù)時候GC至少消耗了一些CPU資源而沒有執(zhí)行應用程序的代碼读存,因此CMS通常比CPU綁定應用程序中的并行GC更差为流。
G1 – Garbage First
G1的一個關鍵設計目標是使由于垃圾收集而導致的stop-the-word暫停的持續(xù)時間和分布是可預測和可配置的,實際上Garbage-First是一個軟實時垃圾收集器让簿,這意味著可以為其設置特定的性能目標敬察。可以在任何給定的y毫秒長時間范圍內請求stop-the-word暫停不超過x毫秒尔当,例如在任何給定秒內不超過5毫秒静汤。Garbage-First
GC將盡最大努力以高概率實現(xiàn)這一目標(但不確定,這將是難以實時的)居凶。
為實現(xiàn)這一目標虫给,G1建立了許多見解。首先侠碧,堆不必分成連續(xù)的Young和Old代抹估。相反,堆被分成多個(通常約2048個)較小的堆區(qū)域弄兜,可以容納對象药蜻。每個區(qū)域可以是伊甸園區(qū)域,幸存者區(qū)域或舊區(qū)域替饿。所有伊甸園和幸存者地區(qū)的邏輯聯(lián)盟都是年輕一代语泽,所有舊區(qū)域都是老一代:
這允許GC避免一次收集整個堆,而是逐步地處理問題:一次只考慮一個區(qū)域的子集视卢,稱為收集集踱卵。在每次暫停期間收集所有Young區(qū)域,但也可以包括一些舊區(qū)域:
G1的另一個新穎之處在于据过,在并發(fā)階段惋砂,它估計每個區(qū)域包含的實時數(shù)據(jù)量。這用于構建集合集:首先收集包含最多垃圾的區(qū)域绳锅。因此名稱:垃圾優(yōu)先收集西饵。
啟用G1收集器的情況下運行JVM
java -XX:+ UseG1GC
Evacuation Pause:Fully Young
在應用程序生命周期的開始階段,G1沒有來自尚未執(zhí)行的并發(fā)階段的任何附加信息鳞芙,因此它最初在完全年輕模式下運行眷柔。當Young
Generation填滿時期虾,應用程序線程被停止,Young區(qū)域內的實時數(shù)據(jù)被復制到Survivor區(qū)域驯嘱,或任何由此成為Survivor的自由區(qū)域彻消。
復制這些的過程稱為Evacuation(疏散),它的工作方式與我們之前看到的其他年輕代收集器的工作方式非常相似宙拉。
Concurrent Marking(并行標記)
?????? G1收集器建立在前一節(jié)中的許多CMS概念的基礎上宾尚,盡管它在許多方面有所不同,但并發(fā)標記的目標是非常相似的谢澈。g1并發(fā)標記使用Snapshot-At-The-Beginning (初始快照)方法來標記標記循環(huán)開始時所有活動的對象煌贴,即使它們同時變成垃圾。關于哪些對象是活動的信息允許為每個區(qū)域建立活動狀態(tài)锥忿,以便以后可以有效地選擇收集集牛郑。
然后,這些信息用于在舊區(qū)域中執(zhí)行垃圾收集敬鬓。如果標記確定某個區(qū)域僅包含垃圾淹朋,或者在stop-the-word期間,對包含垃圾和活動對象的舊區(qū)域,則可以同時進行Evacuation(疏散)钉答。
當堆的總占用量足夠大時础芍,就開始進行并發(fā)標記。默認情況下数尿,它是45%仑性,但這可以通過initiatingEapOccupancyPercent JVM選項更改。與CMS一樣右蹦,G1中的并發(fā)標記包括許多階段诊杆,其中一些階段完全并發(fā),而其中一些階段要求停止應用程序線程
三何陆、G1使用
HotSpot有這么多的垃圾回收器晨汹,,Serial GC贷盲、Parallel GC淘这、Concurrent Mark Sweep GC 這三個GC選擇:
[if !supportLists]1.?????[endif]如果你想要最小化地使用內存和并行開銷,請選Serial GC晃洒;
[if !supportLists]2.?????[endif]如果你想要最大化應用程序的吞吐量慨灭,請選Parallel GC;
[if !supportLists]3.?????[endif]如果你想要最小化GC的中斷或停頓時間球及,請選CMS GC。
G1 GC基本思想
G1 GC是一個壓縮收集器呻疹,它基于回收最大量的垃圾原理進行設計吃引。G1 GC利用遞增筹陵、并行、獨占暫停這些屬性镊尺,通過拷貝方式完成壓縮目標朦佩。此外,它也借助并行庐氮、多階段并行標記這些方式來幫助減少標記语稠、重標記、清除暫停的停頓時間弄砍,讓停頓時間最小化是它的設計目標之一仙畦。
G1的第一論文發(fā)表于2004年,在2012年才在jdk1.7u4中可用音婶。oracle官方計劃在jdk9中將G1變成默認的垃圾收集器慨畸,以替代CMS。G1回收器擁有獨特的垃圾回收策略衣式,這和之前提到的回收器截然不同寸士。從分代上看,G1依然屬于分代型垃圾回收器碴卧,它會區(qū)分年輕代和老年代弱卡,年輕代依然有Eden區(qū)和Survivor區(qū),但從堆的結構上看住册,它并不要求整個Eden區(qū)谐宙、年輕代或者老年代在物理上都是連續(xù)。
綜合來說界弧,G1使用了全新的分區(qū)算法凡蜻,其特點如下所示:
并行性:G1在回收期間,可以有多個GC線程同時工作垢箕,有效利用多核計算能力划栓;
并發(fā)性:G1擁有與應用程序交替執(zhí)行的能力,部分工作可以和應用程序同時執(zhí)行条获,因此忠荞,一般來說,不會在整個回收階段發(fā)生完全阻塞應用程序的情況帅掘;
分代GC:G1依然是一個分代收集器委煤,但是和之前的各類回收器不同,它同時兼顧年輕代和老年代修档。對比其他回收器碧绞,或者工作在年輕代,或者工作在老年代吱窝;
空間整理:G1在回收過程中讥邻,會進行適當?shù)膶ο笠苿悠染福幌馛MS只是簡單地標記清理對象。在若干次GC后兴使,CMS必須進行一次碎片整理系宜。而G1不同,它每次回收都會有效地復制對象发魄,減少空間碎片盹牧,進而提升內部循環(huán)速度。
可預見性:由于分區(qū)的原因励幼,G1可以只選取部分區(qū)域進行內存回收汰寓,這樣縮小了回收的范圍,因此對于全局停頓情況的發(fā)生也能得到較好的控制赏淌。
隨著G1 GC的出現(xiàn)踩寇,GC從傳統(tǒng)的連續(xù)堆內存布局設計,逐漸走向不連續(xù)內存塊六水,這是通過引入Region概念實現(xiàn)俺孙,也就是說,由一堆不連續(xù)的Region組成了堆內存掷贾。其實也不能說是不連續(xù)的睛榄,只是它從傳統(tǒng)的物理連續(xù)逐漸改變?yōu)檫壿嬌系倪B續(xù),這是通過Region的動態(tài)分配方式實現(xiàn)的想帅,我們可以把一個Region分配給Eden场靴、Survivor、老年代港准、大對象區(qū)間旨剥、空閑區(qū)間等的任意一個,而不是固定它的作用浅缸,因為越是固定轨帜,越是呆板。
G1 GC垃圾回收機制
首先衩椒,G1的設計原則就是簡單可行的性能調優(yōu)開發(fā)人員僅僅需要聲明以下參數(shù)即可:
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
其中-XX:+UseG1GC為開啟G1垃圾收集器蚌父,-Xmx32g 設計堆內存的最大內存為32G,-XX:MaxGCPauseMillis=200設置GC的最大暫停時間為200ms毛萌。如果我們需要調優(yōu)苟弛,在內存大小一定的情況下,我們只需要修改最大暫停時間即可阁将。其次膏秫,G1將新生代,老年代的物理空間劃分取消了冀痕。這樣我們再也不用單獨的空間對每個代進行設置了荔睹,不用擔心每個代內存是否足夠狸演。
取而代之的是言蛇,G1算法將堆劃分為若干個區(qū)域(Region)僻他,它仍然屬于分代收集器。不過腊尚,這些區(qū)域的一部分包含新生代吨拗,新生代的垃圾收集依然采用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間婿斥。老年代也分成很多區(qū)域劝篷,G1收集器通過將對象從一個區(qū)域復制到另外一個區(qū)域,完成了清理工作民宿。這就意味著娇妓,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮)活鹰,這樣也就不會有cms內存碎片問題的存在了哈恰。?[if !vml]
[endif]在G1中,還有一種特殊的區(qū)域志群,叫Humongous區(qū)域着绷。 如果一個對象占用的空間超過了分區(qū)容量50%以上,G1收集器就認為這是一個巨型對象锌云。這些巨型對象荠医,默認直接會被分配在年老代,但是如果它是一個短期存在的巨型對象桑涎,就會對垃圾收集器造成負面影響彬向。為了解決這個問題,G1劃分了一個Humongous區(qū)攻冷,它用來專門存放巨型對象娃胆。如果一個H區(qū)裝不下一個巨型對象,那么G1會尋找連續(xù)的H分區(qū)來存儲讲衫。為了能找到連續(xù)的H區(qū)缕棵,有時候不得不啟動Full GC。
PS
:在java 8中涉兽,持久代也移動到了普通的堆內存空間中招驴,改為元空間。對象分配策略說起大對象的分配枷畏,我們不得不談談對象的分配策略别厘。它分為3個階段:
TLAB(Thread Local Allocation Buffer)
線程本地分配緩沖區(qū)
Eden
區(qū)中分配
Humongous
區(qū)分配
TLAB為線程本地分配緩沖區(qū),它的目的為了使對象盡可能快的分配出來拥诡。如果對象在一個共享的空間中分配触趴,我們需要采用一些同步機制來管理這些空間內的空閑空間指針氮发。在Eden空間中,每一個線程都有一個固定的分區(qū)用于分配對象冗懦,即一個TLAB爽冕。分配對象時,線程之間不再需要進行任何的同步披蕉。對TLAB空間中無法分配的對象颈畸,JVM會嘗試在Eden空間中進行分配。如果Eden空間無法容納該對象没讲,就只能在老年代中進行分配空間眯娱。最后,G1提供了兩種GC模式爬凑,Young GC和Mixed GC徙缴,兩種都是Stop The World(STW)的。下面我們將分別介紹一下這2種模式嘁信。三于样,G1 Young GC
??? Young GC
主要是對Eden區(qū)進行GC,它在Eden空間耗盡時會被觸發(fā)吱抚。在這種情況下百宇,Eden空間的數(shù)據(jù)移動到Survivor空間中,如果Survivor空間不夠秘豹,Eden空間的部分數(shù)據(jù)會直接晉升到年老代空間携御。Survivor區(qū)的數(shù)據(jù)移動到新的Survivor區(qū)中,也有部分數(shù)據(jù)晉升到老年代空間中既绕。最終Eden空間的數(shù)據(jù)為空啄刹,GC停止工作,應用線程繼續(xù)執(zhí)行凄贩。
這時誓军,需要考慮一個問題,如果僅僅GC 新生代對象疲扎,我們如何找到所有的根呢昵时?老年代的所有對象都是根么?那這樣掃描下來會耗費大量的時間椒丧。于是壹甥,G1引進了RSet的概念。它的全稱是Remembered Set壶熏,作用是跟蹤指向某個heap區(qū)內的對象引用句柠。
[if !vml]
[endif]在CMS中,也有RSet的概念,在老年代中有一塊區(qū)域用來記錄指向新生代的引用溯职。這是一種point-out精盅,在進行Young GC時,掃描根時谜酒,僅僅需要掃描這一塊區(qū)域叹俏,而不需要掃描整個老年代。
但在G1中甚带,并沒有使用point-out她肯,這是由于一個分區(qū)太小佳头,分區(qū)數(shù)量太多鹰贵,如果是用point-out的話,會造成大量的掃描浪費康嘉,有些根本不需要GC的分區(qū)引用也掃描了碉输。于是G1中使用point-in來解決。point-in的意思是哪些分區(qū)引用了當前分區(qū)中的對象亭珍。這樣敷钾,僅僅將這些對象當做根來掃描就避免了無效的掃描。由于新生代有多個肄梨,那么我們需要在新生代之間記錄引用嗎阻荒?這是不必要的,原因在于每次GC時众羡,所有新生代都會被掃描侨赡,所以只需要記錄老年代到新生代之間的引用即可。
需要注意的是粱侣,如果引用的對象很多羊壹,賦值器需要對每個引用做處理,賦值器開銷會很大齐婴,為了解決賦值器開銷這個問題油猫,在G1 中又引入了另外一個概念,卡表(Card Table)柠偶。一個Card Table將一個分區(qū)在邏輯上劃分為固定大小的連續(xù)區(qū)域情妖,每個區(qū)域稱之為卡∮盏#卡通常較小毡证,介于128到512字節(jié)之間。Card
Table通常為字節(jié)數(shù)組该肴,由Card的索引(即數(shù)組下標)來標識每個分區(qū)的空間地址情竹。默認情況下,每個卡都未被引用。當一個地址空間被引用時秦效,這個地址空間對應的數(shù)組索引的值被標記為”0″雏蛮,即標記為臟被引用,此外RSet也將這個數(shù)組下標記錄下來阱州。一般情況下挑秉,這個RSet其實是一個Hash Table,Key是別的Region的起始地址苔货,Value是一個集合犀概,里面的元素是Card Table的Index。
Young GC
階段:階段1:根掃描靜態(tài)和本地對象被掃描階段2:更新RS處理dirty card隊列更新RS
階段3:處理RS檢測從年輕代指向年老代的對象階段4:對象拷貝拷貝存活的對象到survivor/old區(qū)域階段5:處理引用隊列軟引用夜惭,弱引用姻灶,虛引用處理
四,G1 Mix GC
Mix GC
不僅進行正常的新生代垃圾收集诈茧,同時也回收部分后臺掃描線程標記的老年代分區(qū)产喉。它的GC步驟分2步:全局并發(fā)標記(global concurrent marking)拷貝存活對象(evacuation)
在進行Mix GC之前,會先進行global concurrent marking(全局并發(fā)標記)敢会。 global
concurrent marking的執(zhí)行過程是怎樣的呢曾沈?在G1 GC中,它主要是為Mixed GC提供標記服務的鸥昏,并不是一次GC過程的一個必須環(huán)節(jié)塞俱。global concurrent marking的執(zhí)行過程分為五個步驟:初始標記(initial mark,STW)在此階段吏垮,G1 GC 對根進行標記障涯。該階段與常規(guī)的 (STW) 年輕代垃圾回收密切相關。根區(qū)域掃描(root region scan)G1 GC 在初始標記的存活區(qū)掃描對老年代的引用惫皱,并標記被引用的對象像樊。該階段與應用程序(非 STW)同時運行,并且只有完成該階段后旅敷,才能開始下一次 STW 年輕代垃圾回收生棍。并發(fā)標記(Concurrent Marking)G1 GC 在整個堆中查找可訪問的(存活的)對象。該階段與應用程序同時運行媳谁,可以被 STW 年輕代垃圾回收中斷最終標記(Remark涂滴,STW)該階段是 STW 回收,幫助完成標記周期晴音。G1 GC 清空 SATB 緩沖區(qū)柔纵,跟蹤未被訪問的存活對象,并執(zhí)行引用處理锤躁。清除垃圾(Cleanup搁料,STW)在這個最后階段,G1 GC 執(zhí)行統(tǒng)計和 RSet 凈化的STW 操作。在統(tǒng)計期間郭计,G1 GC 會識別完全空閑的區(qū)域和可供進行混合垃圾回收的區(qū)域霸琴。清理階段在將空白區(qū)域重置并返回到空閑列表時為部分并發(fā)。
G1調優(yōu)
CMS最主要解決了pause time昭伸,但是會占用CPU資源梧乘,犧牲吞吐量。CMS默認啟動的回收線程數(shù)是(CPU數(shù)量+3)/ 4庐杨,當CPU<4個時选调,會影響用戶線程的執(zhí)行。另外一個缺點就是內存碎片的問題了灵份,碎片會給大對象的內存分配造成麻煩仁堪,如果老年代的可用的連續(xù)空間也無法分配時,會觸發(fā)full gc各吨。并且full gc時如果發(fā)生young gc會被young gc打斷枝笨,執(zhí)行完young gc之后再繼續(xù)執(zhí)行full gc。
-XX:UseConcMarkSweepGC參數(shù)可以開啟CMS,年輕代使用ParNew揭蜒,老年代使用CMS,同時Serial Old收集器將作為CMS收集器出現(xiàn)Concurrent Mode Failure失敗后的后備收集器使用剔桨。
MaxGCPauseMillis調優(yōu)
G1GC
的最基本的參數(shù):
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
前面2個參數(shù)都好理解屉更,后面這個MaxGCPauseMillis參數(shù)該怎么配置呢?這個參數(shù)從字面的意思上看洒缀,就是允許的GC最大的暫停時間瑰谜。G1盡量確保每次GC暫停的時間都在設置的MaxGCPauseMillis范圍內。 那G1是如何做到最大暫停時間的呢树绩?這涉及到另一個概念萨脑,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的區(qū)域集合饺饭。
Young GC
:選定所有新生代里的region渤早。通過控制新生代的region個數(shù)來控制young GC的開銷。
Mixed GC
:選定所有新生代里的region瘫俊,外加根據(jù)global
concurrent marking統(tǒng)計得出收集收益高的若干老年代region鹊杖。在用戶指定的開銷目標范圍內盡可能選擇收益高的老年代region。
在理解了這些后扛芽,我們再設置最大暫停時間就好辦了骂蓖。首先,我們能容忍的最大暫停時間是有一個限度的川尖,我們需要在這個限度范圍內設置登下。但是應該設置的值是多少呢?我們需要在吞吐量跟MaxGCPauseMillis之間做一個平衡。如果MaxGCPauseMillis設置的過小被芳,那么GC就會頻繁银酬,吞吐量就會下降。如果MaxGCPauseMillis設置的過大筐钟,應用程序暫停時間就會變長揩瞪。G1的默認暫停時間是200毫秒,我們可以從這里入手篓冲,調整合適的時間李破。其他調優(yōu)參數(shù)
-XX:G1HeapRegionSize=n
設置的 G1 區(qū)域的大小。值是 2 的冪壹将,范圍是 1 MB 到 32 MB 之間嗤攻。目標是根據(jù)最小的 Java 堆大小劃分出約 2048 個區(qū)域。
-XX:ParallelGCThreads=n
設置 STW 工作線程數(shù)的值诽俯。將 n 的值設置為邏輯處理器的數(shù)量妇菱。n 的值與邏輯處理器的數(shù)量相同,最多為 8暴区。如果邏輯處理器不止八個闯团,則將 n 的值設置為邏輯處理器數(shù)的5/8 左右。這適用于大多數(shù)情況仙粱,除非是較大的 SPARC 系統(tǒng)房交,其中 n 的值可以是邏輯處理器數(shù)的 5/16 左右。
-XX:ConcGCThreads=n
設置并行標記的線程數(shù)伐割。將 n 設置為并行垃圾回收線程數(shù)(ParallelGCThreads) 的 1/4 左右候味。
-XX:InitiatingHeapOccupancyPercent=45
設置觸發(fā)標記周期的 Java 堆占用率閾值。默認占用率是整個Java 堆的 45%隔心。避免使用以下參數(shù):避免使用 -Xmn 選項或 -XX:NewRatio 等其他相關選項顯式設置年輕代大小纲菌。固定年輕代的大小會覆蓋暫停時間目標深滚。觸發(fā)Full GC在某些情況下,G1觸發(fā)了Full GC,這時G1會退化使用Serial收集器來完成垃圾的清理工作计技,它僅僅使用單線程來完成GC工作返奉,GC暫停時間將達到秒級別的动分。整個應用處于假死狀態(tài)假消,不能處理任何請求,我們的程序當然不希望看到這些耐床。那么發(fā)生Full GC的情況有哪些呢密幔?并發(fā)模式失敗
G1啟動標記周期,但在Mix GC之前撩轰,老年代就被填滿胯甩,這時候G1會放棄標記周期昧廷。這種情形下,需要增加堆大小偎箫,或者調整周期(例如增加線程數(shù)-XX:ConcGCThreads等)木柬。晉升失敗或者疏散失敗
G1在進行GC的時候沒有足夠的內存供存活對象或晉升對象使用,由此觸發(fā)了Full GC淹办∶颊恚可以在日志中看到(to-space exhausted)或者(to-space overflow)。解決這種問題的方式是:
a,
增加 -XX:G1ReservePercent 選項的值(并相應增加總的堆大辛)速挑,為“目標空間”增加預留內存量。
b,
通過減少 -XX:InitiatingHeapOccupancyPercent 提前啟動標記周期副硅。
c,
也可以通過增加 -XX:ConcGCThreads 選項的值來增加并行標記線程的數(shù)目姥宝。巨型對象分配失敗
當巨型對象找不到合適的空間進行分配時,就會啟動Full GC恐疲,來釋放空間腊满。這種情況下,應該避免分配大量的巨型對象培己,增加內存或者增大-XX:G1HeapRegionSize碳蛋,使巨型對象不再是巨型對象。