CMS實現原理


CMS實現原理

簡介

學習過JAVA語言的堆CMS這款垃圾收集器都不會陌生,CMS曾經號稱是並發度最高的垃圾收集器。CMS是一款只能應用於老年代收集的垃圾收集器。CMS為了支持與應用線程同時工作(垃圾收集的時候,業務線程同時工作,修改對象),重載了寫屏障(賦值引用對象被修改的時候,將其壓入標記棧)代碼。在並發標記階段修改的對象必須重新標記使得所有的對象都被標記了。

垃圾收集器可以簡化內存分配和增強魯棒性,但是早期不被程序員所接受,很大一部原因是性能問題。開發者不接收自動垃圾回收,只有兩方面的原因:吞吐量和延遲。計算能力的增加被內存需求增加所抵消了。

分代收集可以較好解決吞吐量和延遲的問題?如何解決呢?將整個堆划分成兩部分,新生代和老年代。

  • 新生代的特性:
    • 存儲新創建的對象
    • 大部分對象都是一些朝生夕死的對象,每次收集可以釋放大部分的空間
    • 通常空間也是相對較小,所以收集較快,不用擔心延遲問題
  • 老年代的特性
    • 新生代對象經過多次收集還存活,會晉升到老年代中
    • 盡管老年代空間較大,總會有填滿的時候,最終會填滿,需要進行回收
    • 對老年的收集同樣存在吞吐量和延遲的問題,分代設計不能解決這個問題

CMS充分利用分代收集系統的優勢,致力於減少最糟糕的情形下垃圾回收的停頓時間,它在大部分的情形下可以和業務線程同時運行,只有極少情況下會掛起業務線程。

並行&&並發

  • 並行:在GC中並行表示多條GC線程並行工作,但此時用戶線程處於等待狀態,在單核CPU中,並行GC效率較低
  • 並發:用戶線程和GC線程同時執行

CMS執行的幾個階段

cms是一個並發的三色算法,該算法使用寫屏障,將變更的對象保持為灰色。cms在三色算法的基礎上做了一些創新,犧牲了完全並發以獲得更高的吞吐量, 它允許在堆根節點變更時不需要保證三色的不變,對根節點(棧,寄存器,全局變量)的更新比堆中的更新通常更頻繁。該算法在處理根節點時,會短暫的掛起應用線程,該算法假設在一個堆中對象的變更頻率較低的基礎之上,否則,在重新標記階段需要掃描大量的臟對象,導致較長的停頓時間。雖然某些程序會打破我們的假設,但是,Boehm et al.的報告中顯示在實踐中這項技術運行良好,尤其是在交互式的應用中。主要由4階段組成:

  • 初始標記階段
    掛起應用線程,標記系統中由根節點對象直接可達的對象
  • 並發標記階段
    恢復應用線程,同時標記所有可達的對象。這個階段不能保證在結束的時候能標記完所有的可達對象,因為應用線程在運行,可能會導致部分引用的變更,導致一些活對象不可達。為了解決這個問題,該算法會通過某種方式跟變更的對象的引用保持聯系。
  • 重新標記階段
    再一次掛起應用線程,將並發標記階段更新過得對象當做根對象再一次掃描標記所有可達的對象,在這個過程可能會導致浮動垃圾(垃圾對象被錯誤標記了),在下一次垃圾回收的時候被回收。
  • 並發清除階段
    再一次恢復應用線程,並發清除整個堆,釋放沒有被標記的對象空間。這個階段必須注意,不能釋放新創建的對象空間。

CMS執行的一個示例

該示例取自一篇牛逼的論文,解釋我們的場景完全足夠。整個堆內存有4頁,包含了7個對象。在初始標記階段,4頁都標記為clean,對象a是從根直接可達的,所以將其標記為活對象。

1a處於並發標記的過程中,對象b,c,e都被標記為活對象。在這個時候,對象g應用d被刪除了,對象b引用c修改為引用d.因為g和b發生了變更,所以第1頁和第3頁被標記為臟頁。

1c表示在並發標記結束時的樣子。很明顯,標記還不完整,因為b的引用對象d還沒有被標記。在重新標記階段才會被標記:在這個階段所有的臟頁會重新掃描,d會被標記上。

1d表示就是重新標記后的狀態,這時候標記就結束了。下一個階段就是並發清除了,最終f會被回收。

在回收的時候,雖然c現在是不可達的對象,但它被標記了,所以不會被回收,它會在下一次垃圾回收的時候會被回收。

CMS收集的設計決策

內存分配

有如下的集中方式,CMS選擇了空閑列表的方式。

  • 標記壓縮:
    • 壓縮之后,內存分配更有效
    • 壓縮之后,需要更新指針指向的地址,但是在並發場景下更新指針是非常困難的
  • 空閑列表
    • 兩個空閑列表保存,一大一小,小對象一個list,大對象一個list

老年代到新生代的掃描

某些場景下,分代收集器需要跟蹤老年代到新生代的引用。CMS使用卡表的方式來解決這個問題。

  • 掃描整個老年代:
    • 采用這種方式,新生代回收基本等於掃描整個堆空間
  • card table:
    • 將整個堆分割成若干個子區域,每一個區域作為一個card, 當該區域的對象有更新時,通過寫屏障將包含該對象的card標記為dirty(即該區域被修改過)。
    • 虛擬內存保護技術可以將頁標記為臟頁,也能實現這個目的,但是使用card table的方式有一些優勢:
      • 開銷小
      • 粒度更細:虛擬內存保護技術使用頁大小為單位,會導致標記為臟頁的對象遠遠超過更新過得對象數,通常最小是4KB, 而CMS中card table可以是512M
      • 更精確的類型信息:虛擬內存保護技術不區分更新的是什么數據,card table可以精確控制引用對象變更才標記為臟頁,前者會導致更多的臟頁

根掃描

標記對象使用額外的bitmap來存儲,沒有直接存儲在對象頭中。避免並發過程中,影響對象頭的訪問。對對象的掃描需要一個額外的數據結構來存儲將要被掃描的對象,隊列或者棧來存儲。

  • 最小化停頓時間
    直接將所有可達的對象放置在這個數據結構中,在垃圾收集器中,內存是稀缺資源,對內存的使用需要謹慎。根對象包含有棧、寄存器、全局變量,此外還包含並發標記階段未被發生變更的對象,這將會導致該數據結構占用的內存特別大,所以不能使用這種方式。
  • 最小化內存開銷
    由根直接可達的對象放置在該數據結構中,這會將所有對象的掃描都當做根掃描的一部分,不適合並發的場景。
  • 前兩種方式的妥協方案
    根直接可達的對象放置在該數據結構中,同時使用bitmap來標記已經被掃描過得對象
    • 執行過程
      • 引用在cur前面,僅標記,不用推到棧中,將在后面的掃描被訪問
      • 引用在cur后面,標記同時將其壓入棧中

        上圖中,e僅僅被標記,a被標記和壓入棧中。

並發清除

隨着堆內存分配和回收,內存塊的大小會逐漸變小,清除回收之后,需要堆內存塊進行合並操作。在非並發的場景,可以直接將所有的空閑列表的空間直接重建就能實現。
在並發收集器中,回收的同時也在做內存分配,這個加排它鎖可以解決。

垃圾收集執行的偽代碼:

initFrac = (1 - heapOccupancyFrac) * allocBeforeCycleFrac;
while (TRUE) {
    sleep(SLEEP_INTERVAL);
    if (generationOccupancy() > initFrac) {
        /* 1st stop-the-world phase */ 
        initialMarkingPause(); 
        concurrentMarkingPhase(); 
        concurrentPreCleaningPhase(); 
        if (markedPercentage() < 98%) {
             
            /* 2nd stop-the-world phase */ 
            finalMarkingPause();
            if (markedPercentage() < 98%) 
                concurrentSweepingPhase();
        }
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM