一篇文章教你弄懂java CMS垃圾回收日志
文章目錄
- 一、CMS垃圾回收器介紹
- 二、CMS JVM運行參數
- 三、CMS收集器運行過程
- 1、初始標記(CMS initial mark)
- 2、并發標記(CMS concurrent mark)
- 3、重新標記(CMS remark)
- 4、并發清除(CMS concurrent sweep)
- 四、什么樣原因會導致FGC
寫在前面: 我是「境里婆娑」。我還是從前那個少年,沒有一絲絲改變,時間只不過是考驗,種在心中信念絲毫未減,眼前這個少年,還是最初那張臉,面前再多艱險不退卻。
寫博客的目的就是分享給大家一起學習交流,如果您對 Java感興趣,可以關注我,我們一起學習。
本篇文章主要介紹程序出現Full Gc問題時,如何查看GC日志,幫助我們快速定位問題。以及使用工具定位FGC。
一、CMS垃圾回收器介紹
- CMS只會回收老年代和永久帶(1.8開始為元數據區,需要設置CMSClassUnloadingEnabled),不會收集年輕帶;
- CMS是一種預處理垃圾回收器,它不能等到old內存用盡時回收,需要在內存用盡前,完成回收操作,否則會導致并發回收失敗;所以cms垃圾回收器開始執行回收操作,有一個觸發閾值,默認是老年代或永久帶達到92%。
CMS 特點
針對老年代;基于"標記-清除"算法(不進行壓縮操作,產生內存碎片); 以獲取最短回收停頓時間為目標;并發收集、低停頓;需要更多的內存(看后面的缺點); 是HotSpot在JDK1.5推出的第一款真正意義上的并發(Concurrent)收集器;一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作;應用場景
與用戶交互較多的場景; 希望系統停頓時間最短,注重服務的響應速度;以給用戶帶來較好的體驗;如常見WEB、B/S系統的服務器上的應用;如果想要詳細了其他垃圾回收器可以看這篇文章:Java虛擬機垃圾回收(三) 7種垃圾收集器
因為本文不涉及詳細介紹CMS垃圾回收器特點,如果想了解可以查看Java官方文章:Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide
二、CMS JVM運行參數
如果你要在生產環境中使用CMS GC,下面這些跟日志相關的參數是必備的,有了這些參數,你才能排查基本的垃圾回收問題。
| -XX:+UseConcMarkSweepGC | 參數指定使用CMS垃圾回收器 |
| -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 | 參數指定CMS垃圾回收器在老年代達到80%的時候開始工作,如果不指定那么默認的值為92% |
| -XX:+CMSClassUnloadingEnabled | 開啟永久帶(jdk1.8以下版本)或元數據區(jdk1.8及其以上版本)收集,如果沒有設置這個標志,一旦永久代或元數據區耗盡空間也會嘗試進行垃圾回收,但是收集不會是并行的,而再一次進行Full GC; |
| -XX:+UseParNewGC | 使用cms時默認這個參數就是打開的,不需要配置,cms只回收老年代,年輕帶只能配合Parallel New或Serial回收器 |
| -XX:+CMSParallelRemarkEnabled | 減少Remark階段暫停的時間,啟用并行Remark,如果Remark階段暫停時間長,可以啟用這個參數 |
| -XX:+CMSScavengeBeforeRemark | 如果Remark階段暫停時間太長,可以啟用這個參數,在Remark執行之前,先做一次ygc。因為這個階段,年輕帶也是cms的gcroot,cms會掃描年輕帶指向老年代對象的引用,如果年輕帶有大量引用需要被掃描,會讓Remark階段耗時增加; |
| -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 | 兩個參數是針對cms垃圾回收器碎片做優化的,CMS是不會移動內存的, 運行時間長了,會產生很多內存碎片, 導致沒有一段連續區域可以存放大對象,出現”promotion failed”、”concurrent mode failure”, 導致fullgc,啟用UseCMSCompactAtFullCollection 在FULL GC的時候, 對年老代的內存進行壓縮。-XX:CMSFullGCsBeforeCompaction=0 則是代表多少次FGC后對老年代做壓縮操作,默認值為0,代表每次都壓縮, 把對象移動到內存的最左邊,可能會影響性能,但是可以消除碎片; |
| -XX:ConcGCThreads=4 | 定義并發CMS過程運行時的線程數。比如value=4意味著CMS周期的所有階段都以4個線程來執行。盡管更多的線程會加快并發CMS過程,但其也會帶來額外的同步開銷。因此,對于特定的應用程序,應該通過測試來判斷增加CMS線程數是否真的能夠帶來性能的提升。如果未設置這個參數,JVM會根據并行收集器中的-XX:ParallelGCThreads參數的值來計算出默認的并行CMS線程數:ParallelGCThreads = (ncpus <=8 ? ncpus : 8+(ncpus-8)*5/8) ,ncpus為cpu個數,ConcGCThreads =(ParallelGCThreads + 3)/4這個參數一般不要自己設置,使用默認就好,除非發現默認的參數有調整的必要; |
| -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses -XX:+ExplicitGCInvokesConcurrent | 開啟foreground CMS GC,CMS gc 有兩種模式,background和foreground,正常的cms gc使用background模式,就是我們平時說的cms gc;當并發收集失敗或者調用了System.gc()的時候,就會導致一次full gc,這個fullgc是不是cms回收,而是Serial單線程回收器,加入了參數后,執行full gc的時候,就變成了CMS foreground gc,它是并行full gc,只會執行cms中stop the world階段的操作,效率比單線程Serial full GC要高;需要注意的是它只會回收old,因為cms收集器是老年代收集器;而正常的Serial收集是包含整個堆的,加入了參數,代表永久帶也會被cms收集; |
| -XX:+PrintGCDetails -XX:+PrintGCCause -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:…/logs/gc.log | 是打印gc日志,其中 -XX:+PrintGCCause 在jdk1.8之后無需設置 |
| -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=…/dump | 是內存溢出時dump堆 |
三、CMS收集器運行過程
CMS收集器大致分為四個過程:初始標記(CMS initial mark)、并發標記(CMS concurrent mark)、重新標記(CMS remark)并發清除(CMS concurrent sweep)
1、初始標記(CMS initial mark)
僅標記一下GC Roots能直接關聯到的對象;速度很快;但需要"Stop The World";初始標記詳細日志。
2020-05-17T14:58:08.997+0800: [GC (CMS Initial Mark) [1 CMS-initial-mark: 22630K(125696K)] 22743K(126848K), 0.0011803 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]GC日志詳細解析:
CMS初始化標記階段(需要stop the world),這個階段標記的是由根(root)可直達的對象(也就是root之下第一層對象),標記期間整個應用線程會停止。老年代容量為125696K,在使用了22630K時觸發了該標記操作;整個堆容量為126848K,在使用了22743K時觸發了改標記,共耗時0.0011803 秒
這是CMS中兩次stop-the-world事件中的一次。這一步的作用是標記存活的對象,有兩部分:
**補充知識點:**在Java語言里,可作為GC Roots對象的包括以下四種:
ps:為了加快此階段處理速度,減少停頓時間,可以開啟初始標記并行化,-XX:+CMSParallelInitialMarkEnabled,同時調大并行標記的線程數,線程數不要超過cpu的核數;
2、并發標記(CMS concurrent mark)
進行GC Roots Tracing的過程;剛才產生的集合中標記出存活對象;應用程序也在運行;并不能保證可以標記出所有的存活對象;詳細GC日志如下。
2020-05-17T14:58:08.998+0800: [CMS-concurrent-mark-start] 2020-05-17T14:58:09.044+0800: [CMS-concurrent-mark: 0.047/0.047 secs] [Times: user=0.08 sys=0.00, real=0.05 secs]并發標記總共花費0.047秒cpu時間和0.047秒時鐘時間(人可感知的時間)
開始并發標記階段,之前被停止的應用線程會重新啟動;從初始化階段標記的所有可達的對象(root之下第一層隊形)出發標記處第一層對象所引用的對象(root之下第二層、三層等等)。
并發標記總結:
從“初始標記”階段標記的對象開始找出所有存活的對象;
因為是并發運行的,在運行期間會發生新生代的對象晉升到老年代、或者是直接在老年代分配對象、或者更新老年代對象的引用關系等等,對于這些對象,都是需要進行重新標記的,否則有些對象就會被遺漏,發生漏標的情況。為了提高重新標記的效率,該階段會把上述對象所在的Card標識為Dirty,后續只需掃描這些Dirty Card的對象,避免掃描整個老年代;
并發標記階段只負責將引用發生改變的Card標記為Dirty狀態,不負責處理;
這個階段因為是并發的容易導致concurrent mode failure
3、重新標記(CMS remark)
為了修正并發標記期間因用戶程序繼續運作而導致標記變動的那一部分對象的標記記錄; 需要"Stop The World",且停頓時間比初始標記稍長,但遠比并發標記短; 采用多線程并行執行來提升效率;詳細日志:
2020-05-17T14:58:09.052+0800: [GC (CMS Final Remark) [YG occupancy: 1048 K (1152 K)][Rescan (parallel) , 0.0010037 secs][weak refs processing, 0.0007176 secs][class unloading, 0.0112964 secs][scrub symbol table, 0.0069825 secs][scrub string table, 0.0009315 secs][1 CMS-remark: 22630K(125696K)] 23678K(126848K), 0.0226336 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]GC日志詳解:
[YG occupancy:1048 K (1152 K)]:年輕代大小為1152 :,當前使用了1048 K
[Rescan (parallel) , 0.0010037 secs]:在應用暫停后重新并發標記所有存活對象,總共耗時0.0010037 秒
[weak refs processing, 0.0007176 secs]:子階段1—處理弱引用,共耗時0.0007176 秒
[class unloading, 0.0112964 secs]:子階段2—卸載已不使用的類,共耗時0.0112964 秒
[scrub symbol table, 0.0069825 secs]:子階段3–清理symbol table
[scrub string table, 0.0008130 secs]:子階段4—清理string table
[1 CMS-remark: 22630K(125696K)] 23678K(126848K), 0.0226336 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] :重新標記,老年代占用23678K,總容量126848K;整個堆占用23678K,總容量126848K。共耗時0.0226336 秒
重新標記總結
前一個階段已經說明,不能標記出老年代全部的存活對象,是因為標記的同時應用程序會改變一些對象引用,這個階段就是用來處理前一個階段因為引用關系改變導致沒有標記到的存活對象的,它會掃描所有標記為Direty的Card
4、并發清除(CMS concurrent sweep)
回收所有的垃圾對象;詳細垃圾回收日志:
1 2020-05-17T14:58:09.094+0800: [CMS-concurrent-sweep: 0.013/0.015 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 2 2020-05-17T14:58:09.095+0800: [CMS-concurrent-reset-start] 3 2020-05-17T14:58:09.098+0800: [CMS-concurrent-reset: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]日志詳解:
1、并發清理總共耗時0.013秒cpu時間和0.015 秒時鐘時間
2、開始并發重置CMS算法內部數據,來未下次垃圾回收做準備
3、并發重置總共耗時0.003秒cpu時間/0.003秒時鐘時間
標記為存活,如下圖所示:
四、什么樣原因會導致FGC
不管YGC還是FGC,都會造成一定程度的程序卡頓(即Stop The World問題:GC線程開始工作,其他工作線程被掛起),即使采用ParNew、CMS或者G1這些更先進的垃圾回收算法,也只是在減少卡頓時間,而并不能完全消除卡頓。
- 大對象:系統一次性加載了過多數據到內存中(比如SQL查詢未做分頁),導致大對象進入了老年代。
- 內存泄漏:頻繁創建了大量對象,但是無法被回收(比如IO對象使用完后未調用close方法釋放資源),先引發FGC,最后導致OOM.
- 程序頻繁生成一些長生命周期的對象,當這些對象的存活年齡超過分代年齡時便會進入老年代,最后引發FGC.
- 程序BUG導致動態生成了很多新類,使得 Metaspace 不斷被占用,先引發FGC,最后導致OOM.
- 代碼中顯式調用了gc方法,包括自己的代碼甚至框架中的代碼。
- JVM參數設置問題:包括總內存大小、新生代和老年代的大小、Eden區和S區的大小、元空間大小、垃圾回收算法等等
排查FGC問題常用工具
JDK的自帶工具,包括jmap、jstat等常用命令:
- 查看堆內存各區域的使用率以及GC情況
jstat -gcutil -h20 pid 1000 - 查看堆內存中的存活對象,并按空間排序
jmap -histo pid | head -n20 - dump堆內存文件
jmap -dump:format=b,file=heap pid - 可視化的堆內存分析工具:JVisualVM、MAT等
本篇文章參考:GC Algorithms: Implementations
———————————————————————————————————
由于本人水平有限,難免有不足,懇請各位大佬不吝賜教!
總結
以上是生活随笔為你收集整理的一篇文章教你弄懂java CMS垃圾回收日志的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 史上最全的SpringCloud入门学习
- 下一篇: 详细介绍注解@Configuration