最近遇到一個頻繁CMS的case,日志如下:
老年代的占用非常少,初步判斷:并非由達到設置的內存比例觸發,有可能是System.gc()或者permGen引發的CMS
【排查過程1】
增加日志,可以清楚看見,3個區的使用量都非常少
而gccause無法定位觸發原因,只打印了CMS兩個暫停階段的信息
于是重新整理了一下background式CMS的觸發點:
1、System.gc() + ExplicitGCInvokesConcurrent引起的
----確實設置了ExplicitGCInvokesConcurrent,但通過Btrace沒有抓到System.gc的調用痕跡,可排除
2、JVM動態計算,并不是old gen占用率達到CMSInitiatingOccupancyFraction才進行回收
----設置了UseCMSInitiatingOccupancyOnly,可排除
3、當old gen的可用內存少于av_promo(每次次YGC時晉升到老年代對象大小的平均值)并且小于max_promotion_in_bytes(eden使用量+from使用量)時會觸發
----小于max_promotion_in_bytes是有可能的,因為NewRatio=1,但少于av_promo的可能性太小,因為無法打印av_promo的值,寫了一個分析gc日志計算晉升對象大小的程序,可排除
4、由permGen回收觸發的
----沒有設置CMSClassUnloadingEnabled,可排除
看上去好像都不滿足觸發CMS的條件,得從另外一個方向排查
【排查過程2】
查看concurrentMarkSweepThread的源碼,關鍵點sleepBeforeNextCycle,意思是每個周期會調用一次shouldConcurrentCollect去判斷是否需要執行CMS
而這個周期默認2秒,經檢查測試環境設置的也是2秒,再看觸發頻率居然達到每秒2-3次!!遠大于設置的2秒
因此,可以排除是由concurrentMarkSweepThread周期性觸發的background式CMS,也和我們設置的CMSInitiatingOccupancyFraction沒半點關系。
繼續從源碼追查,在genCollectedHeap中有一個GC的公共接口,關鍵點是should_do_concurrent_full_gc的判斷
條件成立,會創建一個VM_GenCollectFullConcurrent的VM_operation,加到VMThread的隊列中執行,可以看到System.gc()其中一種情況也是這樣觸發CMS進行并發回收的(另外一種情況是上述background式的CMS)
查看JVM參數,果然設置了GCLockerInvokesConcurrent (ExplicitGCInvokesConcurrent在上面已被排除)
于是屏蔽了GCLockerInvokesConcurrent進行反復測試,沒有再發生過CMS了,而且觸發YGC的GCLocker GCCause也涌現出來,至此,真相浮出水面
【小結與后續】
頻繁觸發CMS的原因找到了:設置了JVM參數GCLockerInvokesConcurrent
這里遺留了三個疑問:
(1)是什么JNI調用觸發了gclocker
(2)什么情況下gclocker觸發ygc還不夠,要調用genCollectionHeap中的collect方法進行回收
(3)對于屏蔽掉GCLockerInvokesConcurrent的測試,ygc的頻率隨著時間不斷增加,平均每秒2-3次(約等于之前的ygc+cms),顯然問題還是存在的,只是沒有觸發CMS
補充:
查看源碼的時候,能看到initial和remark兩個階段會把cause回寫,這也是為什么gccause只能看到CMS Initial Mark和CMS final Remark的原因吧