作者:京東科技 康志興
1 前言
隨著Java的進化程序,涌現出各種不同的垃圾回收器,從串行執行到并行執行,從高吞吐到低延遲,終極目標就是讓開發人員專注于程式的代碼書寫而無需關注記憶體管理,
JDK早期出現的垃圾回收器通常單獨作用于不同分代,到后期出現的G1開始,才可以進行全區域收集,

關于垃圾回收器的基礎知識請翻看前一篇:從原理聊JVM(一):染色標記和垃圾回收演算法
2 串行收集器(Serial)
比較老的收集器,單執行緒,所收集時必須暫停應用的作業執行緒,直到收集結束,但和其他收集器的單執行緒相比更加簡單、高效,
作用于新生代的收集器叫Serial,采用標記復制演算法;作用于年老代的收集器叫Serial Old,采用標記整理演算法,
3 并行收集器(Parallel)
多條垃圾收集執行緒并行作業,在多核CPU下效率更高,但應用執行緒仍然處于等待狀態,
并行收集器也分為ParNew和Parallel Old,可以理解為它們就是Serial和Serial Old的多執行緒并行版本,甚至部分代碼進行了復用,
ParNew較為流行的原因是因為除了Serial只有它能和CMS搭配使用,但自JDK9開始,由于更先進的G1的出現,官方直接取消了單獨指定ParNew的引數-XX:+UseParNewGC,使其并入了CMS收集器,成為它專門處理新生代的組成部分,
而Parallel Old則搭配新生代收集器ParallelScavenge成為名副其實的“吞吐量優先”的搭配組合,
4 ParallelScavenge
ParallelScavenge收集器是面向新生代的垃圾回收器,它和ParNew其實非常類似,使用標記復制演算法并行收集,區別在于二者關注點不同,ParalletScavenge的目標是達到一個可控制的吞吐量(Throughput),更高的吞吐量意味著最大限度的使用處理器的資源來縮短整體的垃圾回收時間,ParalletScavenge有兩個重要引數:
?-XX:MaxGCPauseMillis
收集器將盡力保證記憶體回識訓費的時間不超過用戶設定值,但這是以犧牲吞吐量為代價的,要求用更短的時間來完成垃圾收集,那么系統就需要降低新生代大小,新生代變小了自然垃圾回識訓更加頻繁,每次垃圾回收都有很多必要作業(比如等待所有執行緒到達安全點),那么更頻繁的垃圾回收就導致了整體吞吐量的降低,
?-XX:GCTimeRatioGCTimeRatio
是垃圾收集時間占總時間的比率,換句話說:其表示運行用戶代碼時間是GC運行時間的X倍,比如默認為99,則垃圾收集時間占比應該1/(1+99),這個數越低,運行用戶代碼時間占比越低,
ParallelScavenge收集器還可以通過引數(-XX:+UseAdaptiveSizePolicy)來激活自適應調節策略,激活后,就不需要人工指定新生代的大小(Xmn)、Eden與Survivor區的比例(XX:SurvivorRatio)、晉升年老代物件大小(XX:PretenureSizeThreshold)等細節引數了,虛擬機會根據當前系統的運行情況收集性能監控資訊,動態調整這些引數以提供最合適的停頓時間或者最大的吞吐量,
5 CMS收集器(Concurrent Mark Sweep)
CMS收集器是縮短暫停應用時間(Low Pause)為目標而設計的,最開始CMS僅僅是年老代收集器,后來將ParNew并入作為其年輕代收集器,
相較上述收集器,CMS是第一個無需全程STW而允許部分階段并發執行的收集器,
垃圾回收實際上主要是兩個階段:識別垃圾和回收垃圾,CMS在這兩個階段分別做了努力來降低停頓:
?識別垃圾
CMS將標記程序打散,并將主要的染色標記程序和用戶執行緒同步進行,并通過增量更新方式解決了參考切換帶來的漏標的問題,
?垃圾回收
CMS采用清除演算法,相比復制和整理,清除演算法由于僅處理死亡物件所以不需要任何停頓,
CMS運行步驟
具體來說,CMS整個程序分為4個步驟:
1. 初始標記(Initial Mark)[STW]
初始標記只是標記一下GC Roots能直接關聯到的物件,速度很快,
2. 并發標記(Concurrent Marking)
并發標記階段是標記可回收物件,
3. 重新標記(Remark)[STW]
重新標記階段則是為了修正并發標記期間因用戶程式繼續運作導致標記產生變動的那一部分物件的標記記錄,這個階段暫停時間比初始標記階段稍長一點,但遠比并發標記時間短,
CMS用增量更新來做并發標記,也就是說并發標記程序中,如果某個已經標記為存活的物件增加了對非存活物件的參考,那么將其標記為灰色,然后在重新標記階段將這一部分物件重新掃描,
4. 并發清除(Concurrent Sweep)
清理洗掉掉標記階段判斷的已經死亡的物件,由于不需要移動存活物件,所以這個階段也是可以與用戶執行緒同時并發的,
優點
由于整個程序中消耗最長的并發標記和并發清除程序收集器執行緒都可以與用戶執行緒一起作業,所以,CMS收集器記憶體回收與用戶一起并發執行的,大大減少了暫停時間,
缺點
1. 處理器資源敏感
垃圾回收的執行緒能夠與用戶執行緒同時執行,這樣雖然不會導致STW,但是由于分攤了處理器的計算資源從而導致應用程式變慢,降低了總吞吐量,
2. 記憶體敏感
當垃圾回收和用戶執行緒在同步運行時產生的垃圾,由于已經過了標記階段所以不會標記后清除,這部分垃圾只能等到下一次GC時才會被清除,這就是浮動垃圾問題,
而且由于垃圾回收和用戶執行緒同步運行,所以不能等堆滿了再GC,而是需要預留一部分記憶體來保證GC程序中用戶執行緒仍有可用記憶體,為了降低GC頻率,只能等垃圾攢多一點再觸發GC,那么GC時可供用戶執行緒使用的記憶體就不多了,
如果GC尚未結束用戶執行緒分配記憶體失敗,這個情況叫做“并發失敗”,這時虛擬機會降級使用Serial Old來重新進行一次高吞吐的年老代收集,這樣停頓時間就長了,
線上環境應根據實際情況來調整觸發GC的記憶體使用閾值,該引數為:-XX:CMSInitiatingOccupancyFraction,
3. CMS基于標記清除演算法,所以記憶體碎片過多后,會頻繁觸發Full GC,且不可避免,CMS會在若干次觸發后進行一次記憶體碎片的合并整理,記憶體整理程序涉及存活物件的移動,(在Shenandoah和ZGC出現前)無法并發,
6 G1收集器(Garbage First)
G1收集器相比上述垃圾回收器有了里程碑式的創新,它將堆記憶體劃分多個大小相等的獨立區域(Region),并且能建立“停頓時間模型”,使暫停時間可控,并盡量將-XX:MaxGCPauseMillis(默認200ms)作為停頓目標,根據Oracle官網的描述,G1是一個“軟實時”的收集器,只是盡量保證在目標停頓時間內完成垃圾收集作業,但不能確保一定:
It is important to note that G1 is not a real-time collector. It meets the set pause time target with high probability but not absolute certainty.
能預測的原因是它能避免對整個堆進行全區收集,而是將整個堆分為若干個小的區域(Region),每個Region是單次垃圾回收的最小單元,在系統運行程序中,G1跟蹤各個Region里的垃圾堆積價值大小(所獲得空間大小以及回收所需時間),在后臺維護一個優先串列,每次根據允許的收集時間,優先回收價值最大的Region,從而保證了再有限時間內獲得更高的收集效率,這也是Garbage First名稱的由來,
G1的分代模型
G1也分為年輕代和年老代,但不是固定劃分,而是每個Region根據運行情況動態劃分,
G1還有一個特殊的區域叫Humongous,G1將超過了一個Region容量一半的大物件,都存放在Humongous區域中,如果物件超過了Region大小,則存放在N個連續的Humongous Region中,G1的大多數行為都把Humongous Region作為老年代的一部分來進行看待,

TAMS(Top at mark start)
為了保證垃圾回收程序中的同時Region也能夠被使用,G1為每一個Region設計了兩個名為TAMS的指標,分別是Previous TAMS(PTAMS)、Next TAMS(NTAMS),在并發標記階段開始前,TAMS指標指向Region內占用記憶體的邊界,在并發標記階段中,G1默認指標之上的物件為存活物件不去進行標記,而物件分配時,用戶執行緒直接在指標之上分配,這就保證了掃描行為和物件分配互不干擾,

G1如何判定Region的“價值”
G1運行期間會收集每個Region的價值資訊,比如回收耗時、記憶集的臟卡數量等,通過計算得出每個Region回收的性價比,G1的停頓預測模型就是通過這些資訊,找出在用戶預期時間內獲得更高回收收益的Region組合,
Remembered Sets
G1堆中的每一個Region都有一份Rememberd Set,也叫RSet,它的作用就是為每一個Region記錄哪些Region對其含有參考,

RSet的更新需要執行緒同步處理,由于物件參考變更非常頻繁,如果同步寫卡表消耗非常大,所以通常會把更新資訊存入佇列中再異步更新RSet,這個佇列就叫Dirty Card Queue,
G1的垃圾回收程序

當Eden中無法分配物件時,觸發Young GC,
當年老代占比到達45%時,等待下一次Young GC時進行并發標記,
并發標記結束后馬上執行Mixed GC,
當Mixed GC對記憶體的清理速度趕不上分配新物件的速度時觸發Full GC,G1的Full GC將使用單執行緒(JDK11后改為多執行緒)執行標記整理演算法,所以耗時巨大,
G1的Young GC
觸發時機
當JVM無法在Eden區分配物件時,
回收范圍
Eden區和Survivor區
運行程序(所有階段均STW)
1. 根掃描
將所有Eden區中的GC Root和RSet記錄的外部參考作為掃描存活物件的入口,
2. 更新RSet
通過Dirty Card Queue中的card更新RSet,保證RSet能準確反應老年代對該Region是否存在參考,
3. 處理RSet
將Eden區中被RSet指向的物件標記為存活物件,
4. 物件復制
判斷存活物件的年齡,如果未達到“閾值”,則復制到一個Surviver區中,否則復制到Old區中,如果Surviver空間不夠,則將部分物件直接復制到Old區中,
5. 處理參考
處理軟參考、弱參考、虛參考等,最終清空全部Eden區,這時清理過的記憶體空間沒有記憶體碎片,
G1的Mixed GC
觸發時機
年老代占用空間超過整個堆的45%(可通過引數-XX:InitiatingHeapOccupancyPercent進行設定)
事實上,并不會立刻觸發,而且等待下一次Young GC,同步進行初始標記步驟,
回收范圍
被并發標記過的Region,這些Region是G1通過價值測算動態選中的,
運行程序

1. 初始標記(Initial Marking)[STW]
標記GC Roots直接關聯的物件,并修改TAMS指標的值,值得注意的是,這一階段并不單獨執行,而是在Minor GC時同步完成,所以實際上這個階段沒有額外停頓,
2. 并發標記(Concurrent Marking)
與用戶執行緒并發執行,順著GC Root遞回標記,標記完成后,重新掃描SATB記錄的有參考變動的物件,如果這時發現空的Region則直接將其清空,
3. 重新標記(Remark)[STW]
由于并發標記是并發執行,并發標記結束后,仍然存在少量的參考變動的物件,所以在這個階段可以STW來處理這部分遺留的物件,并且開始計算所有Region的活躍度,
4. 清理(Clean Up)[STW]
根據用戶期望的停頓時間來制定回收計劃,選擇全部是非存活物件的Old區和回收收益較高的Region加入回收集,清空記憶集,重置已經被清理的空的Region(這一步是非STW的),
5. 拷貝(Coping)[STW]
將回收集其中的存活物件復制到空的Region中,最后清空這些舊的Region,
這個階段的演算法和Young GC完全一致,但默認分8次執行完成(可由引數-XX:G1MixedGCCountTarget設定),所以每次清理的回收集包括Eden區、Survivor區和八分之一的Old區,低存活度(垃圾多)的Region清理的較快,所以會被G1優先回收,
混合回收并不一定要進行8次,有一個閾值-XX :G1HeapWastePercent(默認值為10%),意思是允許整個堆記憶體中有10%的空間被浪費,意味著如果發現可以回收的垃圾占堆記憶體的比例低于10%,則不再進行混合回收,
優點
G1相比較之前的垃圾回收器最大的變化是通過化整為零的思路,將堆分為若干個小的Region來減少GC的范圍,從而達到“低延遲”的目的,
并且G1的垃圾回收程序采用標記復制的演算法,避免了空間碎片化的問題,
缺點
1.記憶體占用較高,由于G1磁區比CMS更多,每個Region都需要建立卡表,其中新生代物件變動頻繁,又加大了卡表維護的成本,
2.G1不僅需要通過寫前屏障來更新卡表,還需要寫后屏障來跟蹤并發時的指標變化以實作快照搜索演算法(SATB),這樣雖然相比增量更新演算法能夠減少并發標記和重新標記階段的消耗,但是用戶程式運行時的計算負載就高了,
3.G1和CMS同樣具有“并發回收”的能力,所以垃圾回收的速度如果跟不上用戶創建新物件的速度,那么就會觸發一個Full GC來獲取更多記憶體,通常把期望停頓時間設定為一兩百毫秒或者兩三百毫秒會是比較合理的,
最佳實踐
1.不要設定年輕代大小年輕代大小應當由G1自行控制,設定為固定值將覆寫暫停時間目標
2.暫停時間目標不要過于嚴苛G1為了Young GC能夠縮短時間需要減少Eden區的個數,那么Young GC就會更加頻繁,Mixed GC想要達到停頓目標就需要減少回收的垃圾數量,如果回收速度低于新物件分配速度將引起Full GC,
3.CMS和G1的選擇目前在小記憶體應用上CMS的表現大概率仍然要會優于G1,而在大記憶體應用上G1則大多能發揮其優勢,這個優劣勢的Java堆容量平衡點通常在6GB至8GB之間,
7 總結
在GC的選擇上,同樣是“沒有銀彈”,不同的收集器有著各自的特點和適用場景,即使是Epsilon也會在特定場合下發揮作用,我們應針對不同的業務特征和系統情況選擇最合適的垃圾回收器,而不是一味求新,
參考:
1.《深入理解Java虛擬機》 by 周志明
2.Getting Started with the G1 Garbage Collector
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/551029.html
標籤:Java
下一篇:返回列表
