上一篇:07-垃圾收集算法詳解
如果說收集算法是內存回收的方法論,那么垃圾收集器就是內存回收的具體實現(xiàn)。
雖然我們對各個收集器進行比較,但并非為了挑選出一個最好的收集器。因為直到現(xiàn)在為止還沒有最好的垃圾收集器出現(xiàn),更加沒有萬能的垃圾收集器,我們能做的就是根據(jù)具體應用場景選擇適合自己的垃圾收集器。試想一下:如果有一種四海之內、任何場景下都適用的完美收集器存在,那么我們的Java虛擬機就不會實現(xiàn)那么多不同的垃圾收集器了。
1.Serial收集器
-XX:+UseSerialGC
-XX:+UseSerialOldGC
Serial(串行)收集器是最基本、歷史最悠久的垃圾收集器了。大家看名字就知道這個收集器是一個單線程收集器了。它的 “單線程” 的意義不僅僅意味著它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( “Stop The World” ),直到它收集結束。
新生代采用復制算法,老年代采用標記-整理算法。
虛擬機的設計者們當然知道Stop The World帶來的不良用戶體驗,所以在后續(xù)的垃圾收集器設計中停頓時間在不斷縮短(仍然還有停頓,尋找最優(yōu)秀的垃圾收集器的過程仍然在繼續(xù))。
但是Serial收集器有沒有優(yōu)于其他垃圾收集器的地方呢?當然有,它簡單而高效(與其他收集器的單線程相比)。Serial收集器由于沒有線程交互的開銷,自然可以獲得很高的單線程收集效率。
Serial Old收集器是Serial收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作為CMS收集器的后備方案。
2.Parallel Scavenge收集器
-XX:+UseParallelGC(年輕代)
-XX:+UseParallelOldGC(老年代)
Parallel收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其余行為(控制參數(shù)、收集算法、回收策略等等)和Serial收集器類似。默認的收集線程數(shù)跟cpu核數(shù)相同,當然也可以用參數(shù)(-XX:ParallelGCThreads)指定收集線程數(shù),但是一般不推薦修改。
Parallel Scavenge收集器關注點是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的關注點更多的是用戶線程的停頓時間(提高用戶體驗)。所謂吞吐量就是CPU中用于運行用戶代碼的時間與CPU總消耗時間的比值。 Parallel Scavenge收集器提供了很多參數(shù)供用戶找到最合適的停頓時間或最大吞吐量,如果對于收集器運作不太了解的話,可以選擇把內存管理優(yōu)化交給虛擬機去完成也是一個不錯的選擇。
新生代采用復制算法,老年代采用標記-整理算法。
Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多線程和“標記-整理”算法。在注重吞吐量以及CPU資源的場合,都可以優(yōu)先考慮 Parallel Scavenge收集器和Parallel Old收集器(JDK8默認的新生代和老年代收集器)。
3.ParNew收集器
-XX:+UseParNewGC
ParNew收集器其實跟Parallel收集器很類似,區(qū)別主要在于它可以和CMS收集器配合使用。
新生代采用復制算法,老年代采用標記-整理算法。
它是許多運行在Server模式下的虛擬機的首要選擇,除了Serial收集器外,只有它能與CMS收集器(真正意義上的并發(fā)收集器,后面會介紹到)配合工作。
4.CMS收集器
-XX:+UseConcMarkSweepGC(old)
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。它非常符合在注重用戶體驗的應用上使用,它是HotSpot虛擬機第一款真正意義上的并發(fā)收集器,它第一次實現(xiàn)了讓垃圾收集線程與用戶線程(基本上)同時工作。
從名字中的Mark Sweep這兩個詞可以看出,CMS收集器是一種 “標記-清除”算法實現(xiàn)的,它的運作過程相比于前面幾種垃圾收集器來說更加復雜一些。整個過程分為四個步驟:
- 初始標記: 暫停所有的其他線程(STW),并記錄下gc roots直接能引用的對象,速度很快。
- 并發(fā)標記: 并發(fā)標記階段就是從GC Roots的直接關聯(lián)對象開始遍歷整個對象圖的過程, 這個過程耗時較長但是不需要停頓用戶線程, 可以與垃圾收集線程一起并發(fā)運行。因為用戶程序繼續(xù)運行,可能會有導致已經(jīng)標記過的對象狀態(tài)發(fā)生改變。
- 重新標記: 重新標記階段就是為了修正并發(fā)標記期間因為用戶程序繼續(xù)運行而導致標記產(chǎn)生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比并發(fā)標記階段時間短。主要用到三色標記里的增量更新算法(見下面詳解)做重新標記。
- 并發(fā)清理: 開啟用戶線程,同時GC線程開始對未標記的區(qū)域做清掃。這個階段如果有新增對象會被標記為黑色不做任何處理(見下面三色標記算法詳解)。
- 并發(fā)重置:重置本次GC過程中的標記數(shù)據(jù)。
從它的名字就可以看出它是一款優(yōu)秀的垃圾收集器,主要優(yōu)點:并發(fā)收集、低停頓。但是它有下面幾個明顯的缺點:
- 對CPU資源敏感(會和服務搶資源);
- 無法處理浮動垃圾(在并發(fā)標記和并發(fā)清理階段又產(chǎn)生垃圾,這種浮動垃圾只能等到下一次gc再清理了);
- 它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產(chǎn)生,當然通過參數(shù)-XX:+UseCMSCompactAtFullCollection可以讓jvm在執(zhí)行完標記清除后再做整理
- 執(zhí)行過程中的不確定性,會存在上一次垃圾回收還沒執(zhí)行完,然后垃圾回收又被觸發(fā)的情況,特別- 是在并發(fā)標記和并發(fā)清理階段會出現(xiàn),一邊回收,系統(tǒng)一邊運行,也許沒回收完就再次觸發(fā)full gc,也就是"concurrent mode failure",此時會進入stop the world,用serial old垃圾收集器來回收
CMS的相關核心參數(shù)
- -XX:+UseConcMarkSweepGC:啟用cms
- -XX:ConcGCThreads:并發(fā)的GC線程數(shù)
- -XX:+UseCMSCompactAtFullCollection:FullGC之后做壓縮整理(減少碎片)
- -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后壓縮一次,默認是0,代表每次FullGC后都會壓縮一次
- -XX:CMSInitiatingOccupancyFraction: 當老年代使用達到該比例時會觸發(fā)FullGC(默認是92,這是百分比)
- -XX:+UseCMSInitiatingOccupancyOnly:只使用設定的回收閾值(-XX:CMSInitiatingOccupancyFraction設定的值),如果不指定,JVM僅在第一次使用設定值,后續(xù)則會自動調整
- -XX:+CMSScavengeBeforeRemark:在CMS GC前啟動一次minor gc,降低CMS GC標記階段(也會對年輕代一起做標記,如果在minor gc就干掉了很多對垃圾對象,標記階段就會減少一些標記時間)時的開銷,一般CMS的GC耗時 80%都在標記階段
- -XX:+CMSParallellnitialMarkEnabled:表示在初始標記的時候多線程執(zhí)行,縮短STW
- -XX:+CMSParallelRemarkEnabled:在重新標記的時候多線程執(zhí)行,縮短STW;
5.G1收集器
-XX:+UseG1GC
G1 (Garbage-First)是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量性能特征.
G1將Java堆劃分為多個大小相等的獨立區(qū)域(Region),JVM目標是不超過2048個Region(JVM源碼里TARGET_REGION_NUMBER 定義),實際可以超過該值,但是不推薦。
一般Region大小等于堆大小除以2048,比如堆大小為4096M,則Region大小為2M,當然也可以用參數(shù)"-XX:G1HeapRegionSize"手動指定Region大小,但是推薦默認的計算方式。
G1保留了年輕代和老年代的概念,但不再是物理隔閡了,它們都是(可以不連續(xù))Region的集合。
默認年輕代對堆內存的占比是5%,如果堆大小為4096M,那么年輕代占據(jù)200MB左右的內存,對應大概是100個Region,可以通過“-XX:G1NewSizePercent”設置新生代初始占比,在系統(tǒng)運行中,JVM會不停的給年輕代增加更多的Region,但是最多新生代的占比不會超過60%,可以通過“-XX:G1MaxNewSizePercent”調整。年輕代中的Eden和Survivor對應的region也跟之前一樣,默認8:1:1,假設年輕代現(xiàn)在有1000個region,eden區(qū)對應800個,s0對應100個,s1對應100個。
一個Region可能之前是年輕代,如果Region進行了垃圾回收,之后可能又會變成老年代,也就是說Region的區(qū)域功能可能會動態(tài)變化。
G1垃圾收集器對于對象什么時候會轉移到老年代跟之前講過的原則一樣,唯一不同的是對大對象的處理,G1有專門分配大對象的Region叫Humongous區(qū),而不是讓大對象直接進入老年代的Region中。在G1中,大對象的判定規(guī)則就是一個大對象超過了一個Region大小的50%,比如按照上面算的,每個Region是2M,只要一個大對象超過了1M,就會被放入Humongous中,而且一個大對象如果太大,可能會橫跨多個Region來存放。
Humongous區(qū)專門存放短期巨型對象,不用直接進老年代,可以節(jié)約老年代的空間,避免因為老年代空間不夠的GC開銷。
Full GC的時候除了收集年輕代和老年代之外,也會將Humongous區(qū)一并回收。
G1收集器一次GC(主要值Mixed GC)的運作過程大致分為以下幾個步驟:
- 初始標記(initial mark,STW):暫停所有的其他線程,并記錄下gc roots直接能引用的對象,速度很快 ;
- 并發(fā)標記(Concurrent Marking):同CMS的并發(fā)標記
- 最終標記(Remark,STW):同CMS的重新標記
- 篩選回收(Cleanup,STW):篩選回收階段首先對各個Region的回收價值和成本進行排序,根據(jù)用戶所期望的GC停頓STW時間(可以用JVM參數(shù) -XX:MaxGCPauseMillis指定)來制定回收計劃,比如說老年代此時有1000個Region都滿了,但是因為根據(jù)預期停頓時間,本次垃圾回收可能只能停頓200毫秒,那么通過之前回收成本計算得知,可能回收其中800個Region剛好需要200ms,那么就只會回收800個Region(Collection Set,要回收的集合),盡量把GC導致的停頓時間控制在我們指定的范圍內。這個階段其實也可以做到與用戶程序一起并發(fā)執(zhí)行,但是因為只回收一部分Region,時間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率。不管是年輕代或是老年代,回收算法主要用的是復制算法,將一個region中的存活對象復制到另一個region中,這種不會像CMS那樣回收完因為有很多內存碎片還需要整理一次,G1采用復制算法回收幾乎不會有太多內存碎片。(注意:CMS回收階段是跟用戶線程一起并發(fā)執(zhí)行的,G1因為內部實現(xiàn)太復雜暫時沒實現(xiàn)并發(fā)回收,不過到了ZGC,Shenandoah就實現(xiàn)了并發(fā)收集,Shenandoah可以看成是G1的升級版本)
G1收集器在后臺維護了一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來),比如一個Region花200ms能回收10M垃圾,另外一個Region花50ms能回收20M垃圾,在回收時間有限情況下,G1當然會優(yōu)先選擇后面這個Region回收。這種使用Region劃分內存空間以及有優(yōu)先級的區(qū)域回收方式,保證了G1收集器在有限時間內可以盡可能高的收集效率。
被視為JDK1.7以上版本Java虛擬機的一個重要進化特征。它具備以下特點:
- 并行與并發(fā):G1能充分利用CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓時間。部分其他收集器原本需要停頓Java線程來執(zhí)行GC動作,G1收集器仍然可以通過并發(fā)的方式讓java程序繼續(xù)執(zhí)行。
- 分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。
- 空間整合:與CMS的“標記–清理”算法不同,G1從整體來看是基于“標記整理”算法實現(xiàn)的收集器;從局部上來看是基于“復制”算法實現(xiàn)的。
- 可預測的停頓:這是G1相對于CMS的另一個大優(yōu)勢,降低停頓時間是G1 和 CMS 共同的關注點,但G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段(通過參數(shù)"-XX:MaxGCPauseMillis"指定)內完成垃圾收集。
毫無疑問, 可以由用戶指定期望的停頓時間是G1收集器很強大的一個功能, 設置不同的期望停頓時間, 可使得G1在不同應用場景中取得關注吞吐量和關注延遲之間的最佳平衡。 不過, 這里設置的“期望值”必須是符合實際的, 不能異想天開, 畢竟G1是要凍結用戶線程來復制對象的, 這個停頓時
間再怎么低也得有個限度。 它默認的停頓目標為兩百毫秒, 一般來說, 回收階段占到幾十到一百甚至接近兩百毫秒都很正常, 但如果我們把停頓時間調得非常低, 譬如設置為二十毫秒, 很可能出現(xiàn)的結果就是由于停頓目標時間太短, 導致每次選出來的回收集只占堆內存很小的一部分, 收集器收集的速度逐漸跟不上分配器分配的速度, 導致垃圾慢慢堆積。 很可能一開始收集器還能從空閑的堆內存中獲得一些喘息的時間, 但應用運行時間一長就不行了, 最終占滿堆引發(fā)Full GC反而降低性能, 所以通常把期望停頓時間設置為一兩百毫秒或者兩三百毫秒會是比較合理的。
G1垃圾收集分類
YoungGC
YoungGC并不是說現(xiàn)有的Eden區(qū)放滿了就會馬上觸發(fā),G1會計算下現(xiàn)在Eden區(qū)回收大概要多久時間,如果回收時間遠遠小于參數(shù) -XX:MaxGCPauseMills 設定的值,那么增加年輕代的region,繼續(xù)給新對象存放,不會馬上做Young GC,直到下一次Eden區(qū)放滿,G1計算回收時間接近參數(shù) -XX:MaxGCPauseMills 設定的值,那么就會觸發(fā)Young GC
MixedGC
不是FullGC,老年代的堆占有率達到參數(shù)(-XX:InitiatingHeapOccupancyPercent)設定的值則觸發(fā),回收所有的Young和部分Old(根據(jù)期望的GC停頓時間確定old區(qū)垃圾收集的優(yōu)先順序)以及大對象區(qū),正常情況G1的垃圾收集是先做MixedGC,主要使用復制算法,需要把各個region中存活的對象拷貝到別的region里去,拷貝過程中如果發(fā)現(xiàn)沒有足夠的空region能夠承載拷貝對象就會觸發(fā)一次Full GC
Full GC
停止系統(tǒng)程序,然后采用單線程進行標記、清理和壓縮整理,好空閑出來一批Region來供下一次MixedGC使用,這個過程是非常耗時的。(Shenandoah優(yōu)化成多線程收集了)
G1收集器參數(shù)設置
- -XX:+UseG1GC:使用G1收集器
- -XX:ParallelGCThreads:指定GC工作的線程數(shù)量
- -XX:G1HeapRegionSize:指定分區(qū)大小(1MB~32MB,且必須是2的N次冪),默認將整堆劃分為2048個分區(qū)
- -XX:MaxGCPauseMillis:目標暫停時間(默認200ms)
- -XX:G1NewSizePercent:新生代內存初始空間(默認整堆5%,值配置整數(shù),默認就是百分比)
- -XX:G1MaxNewSizePercent:新生代內存最大空間
- -XX:TargetSurvivorRatio:Survivor區(qū)的填充容量(默認50%),Survivor區(qū)域里的一批對象(年齡1+年齡2+年齡n的多個年齡對象)總和超過了Survivor區(qū)域的50%,此時就會把年齡n(含)以上的對象都放入老年代
- -XX:MaxTenuringThreshold:最大年齡閾值(默認15)
- -XX:InitiatingHeapOccupancyPercent:老年代占用空間達到整堆內存閾值(默認45%),則執(zhí)行新生代和老年代的混合收集(MixedGC),比如我們之前說的堆默認有2048個region,如果有接近1000個region都是老年代的region,則可能就要觸發(fā)MixedGC了
- -XX:G1MixedGCLiveThresholdPercent(默認85%) region中的存活對象低于這個值時才會回收該region,如果超過這個值,存活對象過多,回收的的意義不大。
- -XX:G1MixedGCCountTarget:在一次回收過程中指定做幾次篩選回收(默認8次),在最后一個篩選回收階段可以回收一會,然后暫?;厥眨謴拖到y(tǒng)運行,一會再開始回收,這樣可以讓系統(tǒng)不至于單次停頓時間過長。
- -XX:G1HeapWastePercent(默認5%): gc過程中空出來的region是否充足閾值,在混合回收的時候,對Region回收都是基于復制算法進行的,都是把要回收的Region里的存活對象放入其他Region,然后這個Region中的垃圾對象全部清理掉,這樣的話在回收過程就會不斷空出來新的Region,一旦空閑出來的Region數(shù)量達到了堆內存的5%,此時就會立即停止混合回收,意味著本次混合回收就結束了。
G1垃圾收集器優(yōu)化建議
假設參數(shù) -XX:MaxGCPauseMills 設置的值很大,導致系統(tǒng)運行很久才會做年輕代gc,年輕代可能都占用了堆內存的60%了,此時才觸發(fā)年輕代gc。那么存活下來的對象可能就會很多,此時就會導致Survivor區(qū)域放不下那么多的對象,就會進入老年代中。
或者是你年輕代gc過后,存活下來的對象過多,導致進入Survivor區(qū)域后觸發(fā)了動態(tài)年齡判定規(guī)則,達到了Survivor區(qū)域的50%,也會快速導致一些對象進入老年代中。
所以這里核心還是在于調節(jié) -XX:MaxGCPauseMills 這個參數(shù)的值,在保證他的年輕代gc別太頻繁的同時,還得考慮每次gc過后的存活對象有多少,避免存活對象太多快速進入老年代,頻繁觸發(fā)mixed gc.
什么場景適合使用G1
- 50%以上的堆被存活對象占用
- 對象分配和晉升的速度變化非常大
- 垃圾回收時間特別長,超過1秒
- 8GB以上的堆內存(建議值)
- 停頓時間是500ms以內
每秒幾十萬并發(fā)的系統(tǒng)如何優(yōu)化JVM
Kafka類似的支撐高并發(fā)消息系統(tǒng)大家肯定不陌生,對于kafka來說,每秒處理幾萬甚至幾十萬消息時很正常的,一般來說部署kafka需要用大內存機器(比如64G),也就是說可以給年輕代分配個三四十G的內存用來支撐高并發(fā)處理,這里就涉及到一個問題了,我們以前常說的對于eden區(qū)的young gc是很快的,這種情況下它的執(zhí)行還會很快嗎?很顯然,不可能,因為內存太大,處理還是要花不少時間的,假設三四十G內存回收可能最快也要幾秒鐘,按kafka這個并發(fā)量放滿三四十G的eden區(qū)可能也就一兩分鐘吧,那么意味著整個系統(tǒng)每運行一兩分鐘就會因為young gc卡頓幾秒鐘沒法處理新消息,顯然是不行的。那么對于這種情況如何優(yōu)化了,我們可以使用G1收集器,設置 -XX:MaxGCPauseMills 為50ms,假設50ms能夠回收三到四個G內存,然后50ms的卡頓其實完全能夠接受,用戶幾乎無感知,那么整個系統(tǒng)就可以在卡頓幾乎無感知的情況下一邊處理業(yè)務一邊收集垃圾。
G1天生就適合這種大內存機器的JVM運行,可以比較完美的解決大內存垃圾回收時間過長的問題。
6.ZGC收集器
-XX:+UseZGC
ZGC是一款JDK 11中新加入的具有實驗性質的低延遲垃圾收集器,ZGC可以說源自于是Azul System公司開發(fā)的C4(Concurrent Continuously Compacting Collector) 收集器。
ZGC目標
如下圖所示,ZGC的目標主要有4個:
- 支持TB量級的堆。我們生產(chǎn)環(huán)境的硬盤還沒有上TB呢,這應該可以滿足未來十年內,所有JAVA應用的需求了吧。
- 支最大GC停頓時間不超10ms。目前一般線上環(huán)境運行良好的JAVA應用Minor GC停頓時間在10ms左右,Major GC一般都需要100ms以上(G1可以調節(jié)停頓時間,但是如果調的過低的話,反而會適得其反),之所以能做到這一點是因為它的停頓時間主要跟Root掃描有關,而Root數(shù)量和堆大小是沒有任何關系的。
- 支奠定未來GC特性的基礎。
- 支最糟糕的情況下吞吐量會降低15%。這都不是事,停頓時間足夠優(yōu)秀。至于吞吐量,通過擴容分分鐘解決。
另外,Oracle官方提到了它最大的優(yōu)點是:它的停頓時間不會隨著堆的增大而增長!也就是說,幾十G堆的停頓時間是10ms以下,幾百G甚至上T堆的停頓時間也是10ms以下。
不分代(暫時)
單代,即ZGC「沒有分代」。我們知道以前的垃圾回收器之所以分代,是因為源于“「大部分對象朝生夕死」”的假設,事實上大部分系統(tǒng)的對象分配行為也確實符合這個假設。
那么為什么ZGC就不分代呢?因為分代實現(xiàn)起來麻煩,作者就先實現(xiàn)出一個比較簡單可用的單代版本,后續(xù)會優(yōu)化。
ZGC內存布局
ZGC收集器是一款基于Region內存布局的, 暫時不設分代的, 使用了讀屏障、 顏色指針等技術來實現(xiàn)可并發(fā)的標記-整理算法的, 以低延遲為首要目標的一款垃圾收集器。
ZGC的Region可以具有如圖3-19所示的大、 中、 小三類容量:
- 小型Region(Small Region) : 容量固定為2MB, 用于放置小于256KB的小對象。
- 中型Region(Medium Region) : 容量固定為32MB, 用于放置大于等于256KB但小于4MB的對象。
- 大型Region(Large Region) : 容量不固定, 可以動態(tài)變化, 但必須為2MB的整數(shù)倍, 用于放置4MB或以上的大對象。 每個大型Region中只會存放一個大對象, 這也預示著雖然名字叫作“大型Region”, 但它的實際容量完全有可能小于中型Region, 最小容量可低至4MB。 大型Region在ZGC的實現(xiàn)中是不會被重分配(重分配是ZGC的一種處理動作, 用于復制對象的收集器階段, 稍后會介紹到)的, 因為復制一個大對象的代價非常高昂。
NUMA-aware
NUMA對應的有UMA,UMA即Uniform Memory Access Architecture,NUMA就是Non Uniform Memory Access Architecture。UMA表示內存只有一塊,所有CPU都去訪問這一塊內存,那么就會存在競爭問題(爭奪內存總線訪問權),有競爭就會有鎖,有鎖效率就會受到影響,而且CPU核心數(shù)越多,競爭就越激烈。NUMA的話每個CPU對應有一塊內存,且這塊內存在主板上離這個CPU是最近的,每個CPU優(yōu)先訪問這塊內存,那效率自然就提高了:
服務器的NUMA架構在中大型系統(tǒng)上一直非常盛行,也是高性能的解決方案,尤其在系統(tǒng)延遲方面表現(xiàn)都很優(yōu)秀。ZGC是能自動感知NUMA架構并充分利用NUMA架構特性的。
ZGC運作過程
ZGC的運作過程大致可劃分為以下四個大的階段:
- 并發(fā)標記(Concurrent Mark):與G1一樣,并發(fā)標記是遍歷對象圖做可達性分析的階段,它的初始標記(Mark Start)和最終標記(Mark End)也會出現(xiàn)短暫的停頓,與G1不同的是, ZGC的標記是在指針上而不是在對象上進行的, 標記階段會更新顏色指針(見下面詳解)中的Marked 0、 Marked 1標志位。
- 并發(fā)預備重分配(Concurrent Prepare for Relocate):這個階段需要根據(jù)特定的查詢條件統(tǒng)計得出本次收集過程要清理哪些Region,將這些Region組成重分配集(Relocation Set)。ZGC每次回收都會掃描所有的Region,用范圍更大的掃描成本換取省去G1中記憶集的維護成本。
- 并發(fā)重分配(Concurrent Relocate):重分配是ZGC執(zhí)行過程中的核心階段,這個過程要把重分配集中的存活對象復制到新的Region上,并為重分配集中的每個Region維護一個轉發(fā)表(Forward Table),記錄從舊對象到新對象的轉向關系。ZGC收集器能僅從引用上就明確得知一個對象是否處于重分配集之中,如果用戶線程此時并發(fā)訪問了位于重分配集中的對象,這次訪問將會被預置的內存屏障(讀屏障(見下面詳解))所截獲,然后立即根據(jù)Region上的轉發(fā)表記錄將訪問轉發(fā)到新復制的對象上,并同時修正更新該引用的值,使其直接指向新對象,ZGC將這種行為稱為指針的“自愈”(Self-Healing)能力。
- ZGC的顏色指針因為“自愈”(Self-Healing)能力,所以只有第一次訪問舊對象會變慢, 一旦重分配集中某個Region的存活對象都復制完畢后,
- 這個Region就可以立即釋放用于新對象的分配,但是轉發(fā)表還得留著不能釋放掉, 因為可能還有訪問在使用這個轉發(fā)表。
- 并發(fā)重映射(Concurrent Remap):重映射所做的就是修正整個堆中指向重分配集中舊對象的所有引用,但是ZGC中對象引用存在“自愈”功能,所以這個重映射操作并不是很迫切。ZGC很巧妙地把并發(fā)重映射階段要做的工作,合并到了下一次垃圾收集循環(huán)中的并發(fā)標記階段里去完成,反正它們都是要遍歷所有對象的,這樣合并就節(jié)省了一次遍歷對象圖的開銷。一旦所有指針都被修正之后, 原來記錄新舊對象關系的轉發(fā)表就可以釋放掉了。
顏色指針
Colored Pointers,即顏色指針,如下圖所示,ZGC的核心設計之一。以前的垃圾回收器的GC信息都保存在對象頭中,而ZGC的GC信息保存在指針中。
每個對象有一個64位指針,這64位被分為:
- 18位:預留給以后使用;
- 1位:Finalizable標識,此位與并發(fā)引用處理有關,它表示這個對象只能通過finalizer才能訪問;
- 1位:Remapped標識,設置此位的值后,對象未指向relocation set中(relocation set表示需要GC的Region集合);
- 1位:Marked1標識;
- 1位:Marked0標識,和上面的Marked1都是標記對象用于輔助GC;
- 42位:對象的地址(所以它可以支持2^42=4T內存):
為什么有2個mark標記?
每一個GC周期開始時,會交換使用的標記位,使上次GC周期中修正的已標記狀態(tài)失效,所有引用都變成未標記。
GC周期1:使用mark0, 則周期結束所有引用mark標記都會成為01。
GC周期2:使用mark1, 則期待的mark標記10,所有引用都能被重新標記。
通過對配置ZGC后對象指針分析我們可知,對象指針必須是64位,那么ZGC就無法支持32位操作系統(tǒng),同樣的也就無法支持壓縮指針了(CompressedOops,壓縮指針也是32位)。
顏色指針的三大優(yōu)勢:
一旦某個Region的存活對象被移走之后,這個Region立即就能夠被釋放和重用掉,而不必等待整個堆中所有指向該Region的引用都被修正后才能清理,這使得理論上只要還有一個空閑Region,ZGC就能完成收集。
顏色指針可以大幅減少在垃圾收集過程中內存屏障的使用數(shù)量,ZGC只使用了讀屏障。
顏色指針具備強大的擴展性,它可以作為一種可擴展的存儲結構用來記錄更多與對象標記、重定位過程相關的數(shù)據(jù),以便日后進一步提高性能。
讀屏障
之前的GC都是采用Write Barrier,這次ZGC采用了完全不同的方案讀屏障,這個是ZGC一個非常重要的特性。
在標記和移動對象的階段,每次「從堆里對象的引用類型中讀取一個指針」的時候,都需要加上一個Load Barriers。
那么我們該如何理解它呢?看下面的代碼,第一行代碼我們嘗試讀取堆中的一個對象引用obj.fieldA并賦給引用o(fieldA也是一個對象時才會加上讀屏障)。如果這時候對象在GC時被移動了,接下來JVM就會加上一個讀屏障,這個屏障會把讀出的指針更新到對象的新地址上,并且把堆里的這個指針“修正”到原本的字段里。這樣就算GC把對象移動了,讀屏障也會發(fā)現(xiàn)并修正指針,于是應用代碼就永遠都會持有更新后的有效指針,而且不需要STW。
那么,JVM是如何判斷對象被移動過呢?就是利用上面提到的顏色指針,如果指針是Bad Color,那么程序還不能往下執(zhí)行,需要「slow path」,修正指針;如果指針是Good Color,那么正常往下執(zhí)行即可:
? 這個動作是不是非常像JDK并發(fā)中用到的CAS自旋?讀取的值發(fā)現(xiàn)已經(jīng)失效了,需要重新讀取。而ZGC這里是之前持有的指針由于GC后失效了,需要通過讀屏障修正指針。?
后面3行代碼都不需要加讀屏障:Object p = o這行代碼并沒有從堆中讀取數(shù)據(jù);o.doSomething()也沒有從堆中讀取數(shù)據(jù);obj.fieldB不是對象引用,而是原子類型。
正是因為Load Barriers的存在,所以會導致配置ZGC的應用的吞吐量會變低。官方的測試數(shù)據(jù)是需要多出額外4%的開銷:
那么,判斷對象是Bad Color還是Good Color的依據(jù)是什么呢?就是根據(jù)上一段提到的Colored Pointers的4個顏色位。當加上讀屏障時,根據(jù)對象指針中這4位的信息,就能知道當前對象是Bad/Good Color了。
PS:既然低42位指針可以支持4T內存,那么能否通過預約更多位給對象地址來達到支持更大內存的目的呢?答案肯定是不可以。因為目前主板地址總線最寬只有48bit,4位是顏色位,就只剩44位了,所以受限于目前的硬件,ZGC最大只能支持16T的內存,JDK13就把最大支持堆內存從4T擴大到了16T。
ZGC存在的問題
ZGC最大的問題是浮動垃圾。ZGC的停頓時間是在10ms以下,但是ZGC的執(zhí)行時間還是遠遠大于這個時間的。假如ZGC全過程需要執(zhí)行10分鐘,在這個期間由于對象分配速率很高,將創(chuàng)建大量的新對象,這些對象很難進入當次GC,所以只能在下次GC的時候進行回收,這些只能等到下次GC才能回收的對象就是浮動垃圾。
ZGC沒有分代概念,每次都需要進行全堆掃描,導致一些“朝生夕死”的對象沒能及時的被回收。
解決方案
目前唯一的辦法是增大堆的容量,使得程序得到更多的喘息時間,但是這個也是一個治標不治本的方案。如果需要從根本上解決這個問題,還是需要引入分代收集,讓新生對象都在一個專門的區(qū)域中創(chuàng)建,然后專門針對這個區(qū)域進行更頻繁、更快的收集。
ZGC參數(shù)設置
啟用ZGC比較簡單,設置JVM參數(shù)即可:-XX:+UnlockExperimentalVMOptions 「-XX:+UseZGC」。調優(yōu)也并不難,因為ZGC調優(yōu)參數(shù)并不多,遠不像CMS那么復雜。它和G1一樣,可以調優(yōu)的參數(shù)都比較少,大部分工作JVM能很好的自動完成。下圖所示是ZGC可以調優(yōu)的參數(shù):
ZGC觸發(fā)時機
ZGC目前有4中機制觸發(fā)GC:
- 定時觸發(fā),默認為不使用,可通過ZCollectionInterval參數(shù)配置。
- 預熱觸發(fā),最多三次,在堆內存達到10%、20%、30%時觸發(fā),主要時統(tǒng)計GC時間,為其他GC機制使用。
- 分配速率,基于正態(tài)分布統(tǒng)計,計算內存99.9%可能的最大分配速率,以及此速率下內存將要耗盡的時間點,在耗盡之前觸發(fā)GC(耗盡時間 - 一次GC最大持續(xù)時間 - 一次GC檢測周期時間)。
- 主動觸發(fā),(默認開啟,可通過ZProactive參數(shù)配置) 距上次GC堆內存增長10%,或超過5分鐘時,對比距上次GC的間隔時間跟(49 * 一次GC的最大持續(xù)時間),超過則觸發(fā)。
如何選擇垃圾收集器
- 優(yōu)先調整堆的大小讓服務器自己來選擇
- 如果內存小于100M,使用串行收集器
- 如果是單核,并且沒有停頓時間的要求,串行或JVM自己選擇
- 如果允許停頓時間超過1秒,選擇并行或者JVM自己選
- 如果響應時間最重要,并且不能超過1秒,使用并發(fā)收集器
- 4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,幾百G以上用ZGC
下圖有連線的可以搭配使用
JDK 1.8默認使用 Parallel(年輕代和老年代都是)
JDK 1.9默認使用 G1
安全點與安全區(qū)域
安全點就是指代碼中一些特定的位置,當線程運行到這些位置時它的狀態(tài)是確定的,這樣JVM就可以安全的進行一些操作,比如GC等,所以GC不是想什么時候做就立即觸發(fā)的,是需要等待所有線程運行到安全點后才能觸發(fā)。
這些特定的安全點位置主要有以下幾種:
- 方法返回之前
- 調用某個方法之后
- 拋出異常的位置
- 循環(huán)的末尾
大體實現(xiàn)思想是當垃圾收集需要中斷線程的時候, 不直接對線程操作, 僅僅簡單地設置一個標志位, 各個線程執(zhí)行過程時會不停地主動去輪詢這個標志, 一旦發(fā)現(xiàn)中斷標志為真時就自己在最近的安全點上主動中斷掛起。 輪詢標志的地方和安全點是重合的。
安全區(qū)域又是什么?
Safe Point 是對正在執(zhí)行的線程設定的。
如果一個線程處于 Sleep 或中斷狀態(tài),它就不能響應 JVM 的中斷請求,再運行到 Safe Point 上。
因此 JVM 引入了 Safe Region。
Safe Region 是指在一段代碼片段中,引用關系不會發(fā)生變化。在這個區(qū)域內的任意地方開始 GC 都是安全的。文章來源:http://www.zghlxwxcb.cn/news/detail-700337.html
下一篇:09-JVM垃圾收集底層算法實現(xiàn)文章來源地址http://www.zghlxwxcb.cn/news/detail-700337.html
到了這里,關于08-JVM垃圾收集器詳解的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!