1. 程式人生 > >CMS垃圾回收機制

CMS垃圾回收機制

一個 系統 upan 8.0 current 重置 64位 內存 cin

簡介

Concurrent Mark Sweep,是一款基於並發、使用標記清除算法的垃圾回收算法,只針對老年代進行垃圾回收。CMS收集器工作時,GC工作線程和用戶線程可以並發執行,以達到降低STW時間的目的。

開起VM選項-XX:+UseConcMarkSweepGC,表示對老年代的回收采用CMS。

生產環境中常用的兩種垃圾收集器(ParNew:年輕代,CMS:老年代)

CMS維度

根據GC的觸發機制分為:周期性Old GC(被動)和主動Old GC

一般都是被動GC,這裏主要說的也是這個。

主動Old GC的過程,觸發條件比較苛刻:

  • YGC過程發生Promotion Failed,進而對老年代進行回收
  • 比如執行了System.gc(),前提是沒有參數ExplicitGCInvokesConcurrent

如果觸發了主動Old GC,這時周期性Old GC正在執行,那麽會奪過周期性Old GC的執行權(同一個時刻只能有一種在Old GC在運行),並記錄 concurrent mode failure或者 concurrent mode interrupted

CMS收集器特點

  • 盡可能降低停頓
  • 會影響系統整體吞吐量和性能。比如,在用戶線程運行過程中,分一半CPU去做GC,系統性能在GC階段,反應速度就下降一半。
  • 清理不徹底。因為在清理階段,用戶線程還在運行,會產生新的垃圾,無法清理。
  • 不能在空間快滿時再清理,因為和用戶線程一起運行。-XX:CMSInitiatingOccupancyFraction設置觸發GC的閾值,如果不幸內存預留空間不夠,就會引起concurrent mode failure。

STW

首先,我們需要厘清一個概念,即只有標記階段才需要STW (Stop The World)。標記完成以後,需要清除的對象已經確定,無論此時是否產生新的垃圾,都不影響對這些對象的清理。也就是說,清除階段是可以設計成和用戶線程並發執行的。

JVM在暫停的時候,需要選準一個時機,由於JVM系統運行期間的復雜性,不可能做到隨時暫停,因此引入了安全點(safepoint)的概念:程序只有在運行到安全點的時候,才可以暫停下來。HotSpot采用主動中斷的方式,讓執行線程在運行期輪詢是否需要暫停的標誌,若需要則中斷掛起。HotSpot使用了幾條短小精煉的匯編指令便可完成安全點輪詢以及觸發線程中斷,因此對系統性能的影響幾乎可以忽略不計。

可達性

可達性是指,如果一個對象會被至少一個程序中的可達對象通過直接或間接的方式引用,則稱該對象是可達的。更詳細地說,一個對象滿足一下兩個條件之一,即被判定為可達的。

1.本身是根對象。根(root)是指由堆以外空間訪問的對象。JVM會將以下對象標記為根:a.虛擬機棧(棧幀中的本地變量表)中引用的對象;b.方法區中的類靜態屬性引用的對象;c.方法區中的常量引用的對象;d.本地方法棧中JNI的引用對象。

2.被一個可達的對象引用。

CMS的幾個階段

CMS將可達性分析分解成兩個階段:a.僅掃描與根節點直接關聯的對象; b.繼續向下掃描完所有對象。因此,標記階段也被拆分成兩個階段,即初始標記並發標記

CMS完整的收集過程如下:

  1. 初始標記(init-mark):僅掃描與根節點直接關聯的對象並標記,這個階段必須STW, 由於跟節點數量有限,所以這個過程非常短暫。

  2. 並發標記(concurrent-marking):與用戶線程並發標記。這個階段在初始標記的基礎上繼續向下追溯標記。在並發標記階段,用戶線程和標記線程並發執行,所以用戶不會感受到停頓。

    **遍歷第一個階段(Init Mark)標記出來的存活對象,繼續遞歸遍歷老年代,並標記可直接或間接到達的所有老年代存活對象在這個階段,發生變化的對象標記為Dity**

  3. 並發預清理(concurrent-precleaning):與用戶線程並發進行。在並發標記階段一些對象的引用已經發生了變化,precleaning會發現這些引用關系的改變,並將存活的對象標記。舉個例子:如果線程A有一個指向對象X的引用,並將該引用傳遞給了線程B,CMS需要記錄下線程B持有了對象X,即使線程A已經不存在了。precleaning是為了減少下一階段“重新標記”的工作量,因為remark階段會STW

    將會重新掃描前一個階段標記的Dirty對象,並標記被Dirty對象直接或間接引用的對象

  4. 重新標記(remark)remark階段會STW。如果應用正在並發運行且在不斷地改變對象引用,CMS則不能準確地確定某個對象是否存活。所以CMS會在remark階段STW,從而獲取所有引用關系的改變。

  5. 並發清理(concurrent-sweeping):清理垃圾對象,這個階段GC線程和用戶線程並發執行。

  6. 並發重置(concurrent-reset):重置CMS收集器的數據結構,做好下一次執行GC任務的準備工作。

技術分享圖片

可以看出,一個存在2次的STW

promotion failed

垃圾回收時promotion failed是個很頭痛的問題,一般可能是兩種原因產生,第一個原因是救助空間不夠,救助空間裏的對象還不應該被移動到年老代,但年輕代又有很多對象需要放入救助空間;第二個原因是年老代沒有足夠的空間接納來自年輕代的對象;這兩種情況都會轉向Full GC,網站停頓時間較長。

解決方方案一:

第一個原因我的最終解決辦法是去掉救助空間,設置-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0即可,第二個原因我的解決辦法是設置CMSInitiatingOccupancyFraction為某個值(假設70),這樣年老代空間到70%時就開始執行CMS,年老代有足夠的空間接納來自年輕代的對象。

解決方案一的改進方案:

又有改進了,上面方法不太好,因為沒有用到救助空間,所以年老代容易滿,CMS執行會比較頻繁。我改善了一下,還是用救助空間,但是把救助空間加大,這樣也不會有promotion failed。具體操作上,32位Linux和64位Linux好像不一樣,64位系統似乎只要配置MaxTenuringThreshold參數,CMS還是有暫停。為了解決暫停問題和promotion failed問題,最後我設置-XX:SurvivorRatio=1 ,並把MaxTenuringThreshold去掉,這樣即沒有暫停又不會有promotoin failed,而且更重要的是,年老代和永久代上升非常慢(因為好多對象到不了年老代就被回收了),所以CMS執行頻率非常低,好幾個小時才執行一次,這樣,服務器都不用重啟了。

CMSInitiatingOccupancyFraction值與Xmn的關系公式

上面介紹了promontion faild產生的原因是EDEN空間不足的情況下將EDEN與From survivor中的存活對象存入To survivor區時,To survivor區的空間不足,再次晉升到old gen區,而old gen區內存也不夠的情況下產生了promontion faild從而導致full gc.那可以推斷出:eden+from survivor < old gen區剩余內存時,不會出現promontion faild的情況,即:
(Xmx-Xmn)*(1-CMSInitiatingOccupancyFraction/100)>=(Xmn-Xmn/(SurvivorRatior+2)) 進而推斷出:

CMSInitiatingOccupancyFraction <=((Xmx-Xmn)-(Xmn-Xmn/(SurvivorRatior+2)))/(Xmx-Xmn)*100

例如:

當xmx=128 xmn=36 SurvivorRatior=1時 CMSInitiatingOccupancyFraction<=((128.0-36)-(36-36/(1+2)))/(128-36)*100 =73.913

當xmx=128 xmn=24 SurvivorRatior=1時 CMSInitiatingOccupancyFraction<=((128.0-24)-(24-24/(1+2)))/(128-24)*100=84.615…

當xmx=3000 xmn=600 SurvivorRatior=1時 CMSInitiatingOccupancyFraction<=((3000.0-600)-(600-600/(1+2)))/(3000-600)*100=83.33

CMSInitiatingOccupancyFraction低於70% 需要調整xmn或SurvivorRatior值。

參考:

CMS垃圾回收和線上Full GC排查

圖解CMS垃圾回收機制,你值得擁有

Java之CMS GC的7個階段

JVM 之 ParNew 和 CMS 日誌分析

從實際案例聊聊Java應用的GC優化

JVM 學習——垃圾收集器與內存分配策略

CMS垃圾回收機制