《深入理解java虛擬機 第二版 JVM高級特性與最佳實踐》里面提到 CMS 垃圾收集器。
CMS 垃圾收集器的垃圾回收分4個步驟:
- 初始標記(initial mark) 有 STW
- 並發標記(concurrent mark) 沒有 STW
- 重新標記(remark) 有 STW
- 並發清除(concurrent sweep) 沒有 STW
初始標記:僅僅標記 GC Roots 能直接關聯到的對象。
並發標記:對初始標記標記過的對象,進行 trace(進行追蹤,得到所有關聯的對象,進行標記)
重新標記:(原文):為了修正並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄。
CMS 垃圾收集器主要有三個問題:
- 內存碎片(原因是采用了標記-清除算法)
- 對 CPU 資源敏感(原因是並發時和用戶線程一起搶占 CPU)
- 浮動垃圾:在並發標記階段產生了新垃圾不會被及時回收,而是只能等到下一次GC
然后我產生了一個疑問:既然重新標記可以修正並發標記階段的變動,那么為何還有浮動垃圾問題?
網上並沒有找到有人對這個問題進行討論,於是我在 Oracle 官方對 CMS 的介紹中找到了這樣一句話:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html
The second pause comes at the end of the concurrent tracing phase and finds objects that were missed by the concurrent tracing due to updates by the application threads of references in an object after the CMS collector had finished tracing that object. This second pause is referred to as the remark pause.
翻譯:第二次暫停是在並發跟蹤階段結束時進行的,它查找由於CMS收集器完成對對象的引用后,應用程序線程對對象中的引用進行更新而導致並發跟蹤遺漏的對象。該第二暫停稱為重新標記暫停。
以下是我個人對這個問題的解答猜想:
由於標記階段是從 GC Roots 開始標記可達對象,那么在並發標記階段可能產生兩種變動:
- 本來可達的對象,變得不可達了
- 本來不可達的內存,變得可達了
第一種變動會產生所謂的浮動垃圾,第二種變動怎么回事呢?重點在於miss。
如果並發標記階段用戶線程里 new 了一個對象,而它在初始標記和並發標記中是不會能夠從 GC Roots 可達的,也就是were missed。如果沒有重新標記階段來將這個對象標記為可達,那么它會在清理階段被回收,這是嚴重的錯誤,是必須要在重新標記階段來處理的,所以這就是重新標記階段實際上的任務。
相比之下,浮動垃圾是可容忍的問題,而不是錯誤。那么為什么重新標記階段不處理第一種變動呢?也許是由可達變為不可達這樣的變化需要重新從 GC Roots 開始遍歷,相當於再完成一次初始標記和並發標記的工作,這樣不僅前兩個階段變成多余的,浪費了開銷浪費,還會大大增加重新標記階段的開銷,所帶來的暫停時間是追求低延遲的CMS所不能容忍的。
