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