1 名詞解釋
可達性分析算法:用於判斷對象是否存活,基本思想是通過一系列稱為“GC Root”的對象作為起點(常見的GC Root有系統類加載器、棧中的對象、處於激活狀態的線程等),基於對象引用關系,從GC Roots開始向下搜索,所走過的路徑稱為引用鏈,當一個對象到GC Root沒有任何引用鏈相連,證明對象不再存活
Stop The World:GC過程中分析對象引用關系,為了保證分析結果的准確性,需要通過停頓所有Java執行線程,保證引用關系不再動態變化,該停頓事件稱為Stop The World(STW)
Safepoint:代碼執行過程中的一些特殊位置,當線程執行到這些位置的時候,說明虛擬機當前的狀態是安全的,如果有需要GC,線程可以在這個位置暫停。HotSpot采用主動中斷的方式,讓執行線程在運行期輪詢是否需要暫停的標志,若需要則中斷掛起
2 CMS簡介
CMS(Concurrent Mark and Sweep 並發-標記-清除),是一款基於並發、使用標記清除算法的垃圾回收算法,只針對老年代進行垃圾回收。CMS收集器工作時,盡可能讓GC線程和用戶線程並發執行,以達到降低STW時間的目的
通過以下命令行參數,啟用CMS垃圾收集器:
-XX:+UseConcMarkSweepGC
值得補充的是,下面介紹到的CMS GC是指老年代的GC,而Full GC指的是整個堆的GC事件,包括新生代、老年代、元空間等,兩者有所區分
3 新生代垃圾回收
能與CMS搭配使用的新生代垃圾收集器有Serial收集器和ParNew收集器。這2個收集器都采用標記復制算法,都會觸發STW事件,停止所有的應用線程。不同之處在於,Serial是單線程執行,ParNew是多線程執行

4 老年代垃圾回收
CMS GC以獲取最小停頓時間為目的,盡可能減少STW時間,可以分為7個階段

- 階段 1: 初始標記(Initial Mark)
此階段的目標是標記老年代中所有存活的對象, 包括 GC Root 的直接引用, 以及由新生代中存活對象所引用的對象,觸發第一次STW事件
這個過程是支持多線程的(JDK7之前單線程,JDK8之后並行,可通過參數CMSParallelInitialMarkEnabled調整)

- 階段 2: 並發標記(Concurrent Mark)
此階段GC線程和應用線程並發執行,遍歷階段1初始標記出來的存活對象,然后繼續遞歸標記這些對象可達的對象

- 階段 3: 並發預清理(Concurrent Preclean)
此階段GC線程和應用線程也是並發執行,因為階段2是與應用線程並發執行,可能有些引用關系已經發生改變。 通過卡片標記(Card Marking),提前把老年代空間邏輯划分為相等大小的區域(Card),如果引用關系發生改變,JVM會將發生改變的區域標記位“臟區”(Dirty Card),然后在本階段,這些臟區會被找出來,刷新引用關系,清除“臟區”標記

- 階段 4: 並發可取消的預清理(Concurrent Abortable Preclean)
此階段也不停止應用線程. 本階段嘗試在 STW 的 最終標記階段(Final Remark)之前盡可能地多做一些工作,以減少應用暫停時間 在該階段不斷循環處理:標記老年代的可達對象、掃描處理Dirty Card區域中的對象,循環的終止條件有: 1 達到循環次數 2 達到循環執行時間閾值 3 新生代內存使用率達到閾值
- 階段 5: 最終標記(Final Remark)
這是GC事件中第二次(也是最后一次)STW階段,目標是完成老年代中所有存活對象的標記。在此階段執行: 1 遍歷新生代對象,重新標記 2 根據GC Roots,重新標記 3 遍歷老年代的Dirty Card,重新標記
- 階段 6: 並發清除(Concurrent Sweep)
此階段與應用程序並發執行,不需要STW停頓,根據標記結果清除垃圾對象

- 階段 7: 並發重置(Concurrent Reset)
此階段與應用程序並發執行,重置CMS算法相關的內部數據, 為下一次GC循環做准備
5 CMS常見問題
最終標記階段停頓時間過長問題
CMS的GC停頓時間約80%都在最終標記階段(Final Remark),若該階段停頓時間過長,常見原因是新生代對老年代的無效引用,在上一階段的並發可取消預清理階段中,執行閾值時間內未完成循環,來不及觸發Young GC,清理這些無效引用
通過添加參數:-XX:+CMSScavengeBeforeRemark。在執行最終操作之前先觸發Young GC,從而減少新生代對老年代的無效引用,降低最終標記階段的停頓,但如果在上個階段(並發可取消的預清理)已觸發Young GC,也會重復觸發Young GC
並發模式失敗(concurrent mode failure) & 晉升失敗(promotion failed)問題

並發模式失敗:當CMS在執行回收時,新生代發生垃圾回收,同時老年代又沒有足夠的空間容納晉升的對象時,CMS 垃圾回收就會退化成單線程的Full GC。所有的應用線程都會被暫停,老年代中所有的無效對象都被回收

晉升失敗:當新生代發生垃圾回收,老年代有足夠的空間可以容納晉升的對象,但是由於空閑空間的碎片化,導致晉升失敗,此時會觸發單線程且帶壓縮動作的Full GC
並發模式失敗和晉升失敗都會導致長時間的停頓,常見解決思路如下:
- 降低觸發CMS GC的閾值,即參數-XX:CMSInitiatingOccupancyFraction的值,讓CMS GC盡早執行,以保證有足夠的空間
- 增加CMS線程數,即參數-XX:ConcGCThreads,
- 增大老年代空間
- 讓對象盡量在新生代回收,避免進入老年代
內存碎片問題
通常CMS的GC過程基於標記清除算法,不帶壓縮動作,導致越來越多的內存碎片需要壓縮,常見以下場景會觸發內存碎片壓縮:
- 新生代Young GC出現新生代晉升擔保失敗(promotion failed)
- 程序主動執行System.gc()
可通過參數CMSFullGCsBeforeCompaction的值,設置多少次Full GC觸發一次壓縮,默認值為0,代表每次進入Full GC都會觸發壓縮,帶壓縮動作的算法為上面提到的單線程Serial Old算法,暫停時間(STW)時間非常長,需要盡可能減少壓縮時間
參考文檔:https://juejin.im/post/5b6b986c6fb9a04fd1603f4a#heading-9
作者:分布式系統架構
鏈接:https://juejin.im/post/5b6b986c6fb9a04fd1603f4a
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。