九、CMS
一、基礎概念
- cms是以優化停頓時間為目標的一個垃圾收集器
- 采用的是標記-清除算法
- 其實CMS嚴格意義是來說是young區和old區一起回收的
- 流程如下:
- 并行(jdk1.8以后)初始標記,用于標記GC ROOT以及第一個相連的對象(STW,時間短)
- 并發標記,跟用戶線程一起行(時間長)
- 并行重新標記,主要是防止多標和漏標的情況(時間短)
- 并發清理(時間長),這個時候用戶產生的垃圾就放到下一次回收
二、Backgroud CMS(正常模式)
- 這種情況下其實我們的GC流程還可以細分
- 并行初始標記
- 并發標記
- 并發預處理
- 可終止的預處理
- 并行重新標記
- 并發清理
- 為什么會多出來兩個流程呢?
- 主要是為了防止young區引用了old區的對象。假設young區引用了old對象你咋辦,你要全量掃描一次young嗎,慢死你?那就進行一次minorGC
- 并發預處理:主要是在并行重新標記的時候,這個動作是STW,所以并發預處理就是提前先干一次這個活,去標記從young區晉升上來的對象以及old區引用改變的對象
- 可終止的預處理:主要是堆young區進行掃描,看那些對象引用了old區的對象,然后保存在卡表中(流程在4、卡表中),然后這個時候remark的時候就很爽了。
- CMSScheduleRemarkEdenSizeThreshold 默認值:2M
CMSScheduleRemarkEdenPenetration 默認值:50%- 這兩個參數的意思是:當Eden區超過2M的時候開始,當Eden區的內存超過50%就停止可終止的預處理。
- 為什么會設置這么2個參數?
- 主要是想要在Remark的前面發生一次minor GC這個時候卡表里面的數據就會很少了。
- 所以還有一個參數默認是5s就是說當超過5s也會終止。或者如果發生了一次young GC也會終止。
三、記憶集
- 進行young GC時,GC Root除了常見的棧引用、常量、靜態變量、JNI、Class對象等,
- 作用:
- 老年代如果引用了新生代的對象,這種對象也應該要加入到我們的GC Root里面去,因為你進行young GC的時候,總不能全量掃描一遍old區把,這太扯淡了。所以就用記憶集這種數據結構保存這種引用關系。
- 新生代引用了老年代的對象,道理相反
- 記憶集保存非收集對象到收集對象的引用關系的集合。
- 記憶集只是一種思想
四、卡表
- 卡表是記憶集的實現
- 在hotSpot中卡表是一個字節數組,數組中每一項對應內存中某一塊連續的區域。如果區域中的某一個對象引用了待回收的區域的對象,就將這個數組中對應的元素變為1,否則為0
- 卡表是使用一個字節數組實現:CARD_TABLE[],每個元素對應著其標識的內存區域一塊特定大小的內存塊,稱為"卡頁"。hotSpot使用的卡頁是2^9大小,即512字節
- 一個卡頁中可包含多個對象,只要有一個對象的字段存在跨代指針,其對應的卡表的元素標識就變成1,表示該元素變臟,否則為0。GC時,只要篩選本收集區的卡表中變臟的元素加入GC Roots里。
- 流程:
- 并發標記的時候,發現young區里面有引用old區的對象,然后這個A所在的區域標記為臟卡
- 重新標記的時候,就知道這個是被用了的,然后變成正常卡
五、Foregroud CMS(特殊模式)
- CMS的另一種收集模式,只有在正常模式的并發標記階段失敗的時候,才會走到這個模式下
- 什么是并發失敗?
- 在老年代填滿之前不能完成old區的不可達對象的回收。因為這樣話新來的對象是不能放入Old區的,OOM了
- 老年代的有效內存空間不能滿足晉升的需要
- 說直白一點:就是并發標記的時候,內存不足了,OOM了
- 怎樣可以降低并發失敗的幾率呢?
- 通過設置下面的參數,讓old的內存達到一定比例我們就開始回收了,而不是等他滿了在回收
- -XX:CMSInitiatingOccupancyFraction
- -XX:+UseCMSInitiatingOccupancyOnly
- 注意:-XX:+UseCMSInitiatingOccupancyOnly 只是用設定的回收閾值(上面指定的70%),如果不指定,JVM僅在第一次使用設定值,后續則自動調整.這兩個參數表示只有在Old區占了CMSInitiatingOccupancyFraction設置的百分比的內存時才滿足觸發CMS的條件。注意這只是滿足觸發CMS GC的條件。至于什么時候真正觸發CMS GC,由一個后臺掃描線程決定。CMSThread默認2秒鐘掃描一次,判斷是否需要觸發CMS,這個參數可以更改這個掃描時間間隔。
- 如果真出現了并發失敗怎么解決呢?
- 碎片問題也是CMS采用的標記清理算法最讓人詬病的地方:Backgroud CMS采用的標記清理算法會導致內存碎片問題,從而埋下發生FullGC導致長時間STW的隱患
- 通過以下的參數控制,開始進行標記-整理算法。
- -XX:+UseCMSCompactAtFullCollection
- -XX:CMSFullGCsBeforeCompaction=0
- 這兩個參數表示多少次FullGC后采用MSC算法壓縮堆內存,0表示每次FullGC后都會壓縮,同時0也是默認值
- 如果不開啟上面的參數,那就是標記-清除算法
六、三色標記
- 當并發標記的時候,業務線程其實也還在運行,這個時候就有可能產生多標和漏標的情況
- 用三色標記法把GC root可達性標記對象的時候,對于“是否標記”來分顏色
- 三色標記其實官方并沒有這個說法,也沒有這個概念,這個只是因為你看不懂cms源碼,自己YY出來的一個東東
黑色
- 當前對象已經被掃描,且它鏈上所有的對象也都被掃描了
- 黑色對象表示應被掃描完成了,是可以存活的安全對象
- 黑色不能跳過灰色直接指向白色
灰色
- 當前對象已經被掃描,在鏈上它的下一個對象沒有被掃描
白色
- 這個對象沒有被掃描
- 可達性分析之前,所有的對象都是白色
- 可達性分析完了之后,還是白色話就證明你是垃圾
流程
多標-浮動垃圾
- 因為并發標記,當GC線程將某個對象變成黑色后,還沒進入到重新標記階段,業務線程不引用這個對象了。但是這個對象已經是黑色了。
- 并發標記開始后,產生的堆內的垃圾,也會直接變成黑色,不做回收
- 多標,并不會影響正確性,其實還好,下次進行回收就行了。問題不大
漏標
- 因為并發標記,當某個灰色對象引用的白色對象斷開,然后這個白色對象又被黑色對象引用了。那么這個白色對象就成垃圾了,不會被染色了,因為黑色對象是不會在進行掃描標記的。
- 解決方案
- 快照的方式:就是灰色斷開的時候,快照一下,將灰色對象與白色對象的引用保存起來,然后重新標記的時候,就將灰色的為根在標記,這樣把白色的就能變成黑的了
- 增量的方式:當黑色引用白色,就將黑色變成灰色,等待重新標記掃描
- 以上無論是對引用關系記錄的插入還是刪除, 虛擬機的記錄操作都是通過寫屏障實現的。
- 寫屏障實現原始快照(SATB): 當對象B的成員變量的引用發生變化時,比如引用消失(a.b.d = null),我們可以利用寫屏障,將B原來成員變量的引用對象D記錄下來
- 寫屏障實現增量更新: 當對象A的成員變量的引用發生變化時,比如新增引用(a.d = d),我們可以利用寫屏障,將A新的成員變量引用對象D 記錄下來
七、調優參數
CMS標記清除的全局整理
- 由于CMS使用的是標記清除算法,而標記清除算法會有大量的內存碎片的產生,所以JVM提供了
-XX:+UseCMSCompactAtFullCollection參數用于在全局GC(full GC)后進行一次碎片整理的工作 - 由于每次全局GC后都進行碎片整理會較大的影響停頓時間,JVM又提供了參數
-XX:CMSFullGCsBeforeCompaction去 控制在幾次全局GC后會進行碎片整理
CMS常用參數含義
- -XX:+UseConcMarkSweepGC
打開CMS GC收集器。JVM在1.8之前默認使用的是Parallel GC,9以后使用G1 GC。 - -XX:+UseParNewGC
當使用CMS收集器時,默認年輕代使用多線程并行執行垃圾回收(UseConcMarkSweepGC開啟后則默認開啟)。 - -XX:+CMSParallelRemarkEnabled
采用并行標記方式降低停頓(默認開啟)。 - -XX:+CMSConcurrentMTEnabled
被啟用時,并發的CMS階段將以多線程執行(因此,多個GC線程會與所有的應用程序線程并行工作)。(默認開啟) - -XX:ConcGCThreads
定義并發CMS過程運行時的線程數。 - -XX:ParallelGCThreads
定義CMS過程并行收集的線程數。 - -XX:CMSInitiatingOccupancyFraction
該值代表老年代堆空間的使用率,默認值為68。當老年代使用率達到此值之后,并行收集器便開始進行垃圾收集,該參數需要配合UseCMSInitiatingOccupancyOnly一起使用,單獨設置無效。 - -XX:+UseCMSInitiatingOccupancyOnly
該參數啟用后,參數CMSInitiatingOccupancyFraction才會生效。默認關閉。 - -XX:+CMSClassUnloadingEnabled
相對于并行收集器,CMS收集器默認不會對永久代進行垃圾回收。如果希望對永久代進行垃圾回收,可用設置-XX:+CMSClassUnloadingEnabled。默認關閉。 - -XX:+CMSIncrementalMode
開啟CMS收集器的增量模式。增量模式使得回收過程更長,但是暫停時間往往更短。默認關閉。 - -XX:CMSFullGCsBeforeCompaction
設置在執行多少次Full GC后對內存空間進行壓縮整理,默認值0。 - -XX:+CMSScavengeBeforeRemark
在cms gc remark之前做一次ygc,減少gc roots掃描的對象數,從而提高remark的效率,默認關閉。 - -XX:+ExplicitGCInvokesConcurrent
該參數啟用后JVM無論什么時候調用系統GC,都執行CMS GC,而不是Full GC。 - -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
該參數保證當有系統GC調用時,永久代也被包括進CMS垃圾回收的范圍內。 - -XX:+DisableExplicitGC
該參數將使JVM完全忽略系統的GC調用(不管使用的收集器是什么類型)。 - -XX:+UseCompressedOops
這個參數用于對類對象數據進行壓縮處理,提高內存利用率。(默認開啟) - -XX:MaxGCPauseMillis=200
這個參數用于設置GC暫停等待時間,單位為毫秒,不要設置過低。
CMS的線程數計算公式
分young區的parnew gc線程數和old區的cms線程數,分別為以下兩參數:
- -XX:ParallelGCThreads=m // STW暫停時使用的GC線程數,一般用滿CPU
- -XX:ConcGCThreads=n // GC線程和業務線程并發執行時使用的GC線程數,一般較小
ParallelGCThreads
其中ParallelGCThreads 參數的默認值是:
CPU核心數 <= 8,則為 ParallelGCThreads=CPU核心數,比如4C8G取4,8C16G取8
CPU核心數 > 8,則為 ParallelGCThreads = CPU核心數 * 5/8 + 3 向下取整
16核的情況下,ParallelGCThreads = 13
32核的情況下,ParallelGCThreads = 23
64核的情況下,ParallelGCThreads = 43
72核的情況下,ParallelGCThreads = 48
ConcGCThreads
ConcGCThreads的默認值則為:
ConcGCThreads = (ParallelGCThreads + 3)/4 向下取整。
ParallelGCThreads = 1~4時,ConcGCThreads = 1
ParallelGCThreads = 5~8時,ConcGCThreads = 2
ParallelGCThreads = 13~16時,ConcGCThreads = 4
推薦配置
第一種情況:8C16G左右服務器,再大的服務器可以上G1了 沒必要
-Xmx12g -Xms12g
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=2
-XX:+UseConcMarkSweepGC
-XX:+CMSClassUnloadingEnabled
-XX:+CMSIncrementalMode
-XX:+CMSScavengeBeforeRemark
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:CMSFullGCsBeforeCompaction=5
-XX:MaxGCPauseMillis=100 // 按業務情況來定
-XX:+ExplicitGCInvokesConcurrent
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
第二種情況:4C8G
-Xmx6g -Xms6g
-XX:ParallelGCThreads=4
-XX:ConcGCThreads=1
-XX:+UseConcMarkSweepGC
-XX:+CMSClassUnloadingEnabled
-XX:+CMSIncrementalMode
-XX:+CMSScavengeBeforeRemark
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:CMSFullGCsBeforeCompaction=5
-XX:MaxGCPauseMillis=100 // 按業務情況來定
-XX:+ExplicitGCInvokesConcurrent
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
第三種情況:2C4G,這種情況下,也不推薦使用,因為2C的情況下,線程上下文的開銷比較大,性能可能還不如你不動的情況,沒必要。非要用,給你個配置,你自己玩。
-Xmx3g -Xms3g
-XX:ParallelGCThreads=2
-XX:ConcGCThreads=1
-XX:+UseConcMarkSweepGC
-XX:+CMSClassUnloadingEnabled
-XX:+CMSIncrementalMode
-XX:+CMSScavengeBeforeRemark
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:CMSFullGCsBeforeCompaction=5
-XX:MaxGCPauseMillis=100 // 按業務情況來定
-XX:+ExplicitGCInvokesConcurrent
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
八、JDK為什么不選用CMS做垃圾收集器?
- CMS單線程或者雙線程時,效率很低
- CMS可能會導致并發失敗,從而引起full gc
- CMS可終止的預處理在極限狀態會導致5s的STW
- CMS只是針對于停頓時間,在吞吐量上并不是很友好
總結
- 上一篇: DSP vs CPU
- 下一篇: C++17基本教程 第6讲 数组 (tc