CMS垃圾收集器详解
概述
CMS垃圾收集器是一款優秀的老年代并發垃圾收集器,通過與用戶線程并發執行的方式減少GC停頓的時間。本文主要聊一下CMS設計到的相關的數據結構、具體的執行過程、運行中會出現的異常情況。
在CMS之前并行垃圾收集器通過下圖方式進行,雖然GC階段多線程并行執行單此時用戶線程是完全暫停的。如果GC時間過長,將引發服務響應超時、調用接口超時等各類異常。
而CMS垃圾收集器大部分時間GC線程與用戶線程并發執行,只有在初始標記和重新標記階段才暫停用戶線程
總體思路:當達到GC條件時,開始并發標記存活的對象,并發的過程中記錄對象引用關系的變化。并發標記結束后,暫停用戶線程,處理引用關系變化,得到所有存活的對象和可以清理的對象。最終并發清理掉不被使用的對象(存在已經不被引用但本次清理不掉的對象)。
?
初始標記
這是一個STW過程,主要分兩步
1、標記GC Roots可達的老年代對象;
2、遍歷GC Roots下的新生代對象能夠可達的老年代對象;
3、此過程不對以上可達的老年代對象進行進一步的可達掃描。
暫停應用程序線程,遍歷GC ROOTS直接可達的對象并將其壓入標記棧(mark-stack)。標記完之后恢復應用程序線程。
并發標記?
這個階段虛擬機會分出若干線程(GC 線程)去進行并發標記。標記哪些對象呢,標記那些GC ROOTS最終可達的對象。具體做法是推出標記棧里面的對象,然后遞歸標記其直接引用的子對象(如果遇到地址比當前對象低的對象則標記并壓如棧中,遇到地址比當前對象高則只標記不入棧),同樣的把子對象壓到標記棧中,重復推出,壓入。。。直至清空標記棧。這個階段GC線程和應用程序線程同時運行。
標記過程舉例
標記的過程伴隨著對象引用的修改,下圖舉例說明了并發標記的過程
初始標記(圖中未體現,參考步驟a):初始標記時對a進行了標記。
步驟a:對a引用的對象bc,及b引用的對象e進行標記,根據當時的對象引用關系,abcegd是存活的。
步驟b:對象引用關系發生了改變,b不再引用c新增引用d,g不再引用d
步驟c:完成了并發標記的過程,abceg被標記。第二個和第四個區域內的對象引用關系發生了改變,被記錄了下來。這里面運用到了card table、mod union table數據結構和write barrier技術,后面進行說明。
步驟d:實際上是重新標記后的結果,可以看到對象d在并發標記結束時未進行標記,但是它還在被對象b引用,不應該回收。這就依賴重新標記階段對dirty card(對象引用關系發生變化的區域)的處理。
?
這個過程應用線程在運行,可能Young GC也會發生,會發生以下的情況:
1、新生代對象晉升到老年代
2、在老年代分配對象
3、新老年代對象的引用發生變化
三色標記詳解:三色標記法與讀寫屏障 - 簡書
這種條件下可能會出現活動對象的漏標的情況,比如下面場景:
活動對象被遺漏標記
A是活動對象,A->B,標記B可達,將其壓入標記棧,此時A所有直接子對象遍歷完,A出棧,標記線程將不會再訪問A。
同時應用程序移掉了B對C的引用,讓A重新引用C。
B出棧時無法標記C可達,A雖然引用C但標記線程不會再訪問A,此時C會被當成不可達對象。
并發過程中變化的維護card table與mod union table
card table
CMS中一個與YGC相關并十分重要的數據結構是:卡表(card table)。之所以出現卡表這樣的一個數據結構是因為:YGC時為了標記活動標記對象除了tracing GC ROOTS之外, 別忘了老年代里也可能會引用新生代對象。所以正常來說還要掃描一次老年代,如果是掃描整個老年代這將會隨著堆的增大變得越來越慢,特別是現在內存都越來越大了。所以為了提升性能就引入卡表。
卡表提升性能的原理:邏輯上把老年代內存分成一個個大小相等的卡頁(card page,大小512byte),然后對每個卡片準備一個與其對應的標記位,并將這些位集中起管理就好像一個表格(mark table)一樣,當改寫對象引用是從老年代指向新生代時,在老年代對應的卡片標記位上設置標志位即可,通常這樣的卡片我們稱之為dirty card。這項操作可以通過上面的提到的write barrier來實現,這樣就算對象跨多張卡片也不會有什么問題。卡表通常是用byte數組實現的,byte的值只能取[0,1]這兩種。所以btye[i] = 1 就表示第i + 1 卡片所在內存上有指向新生代引用的老年代對象,這時只要tracing這個卡片上的對象即可。如果每個card大小的是128字節(1024位,)那卡表就只占整個老年代的1/1024之一。所以遍歷卡表的時間會遠比遍歷整個老年代快得多!這其中背后思想就是典型以空間換時間的思路!這種思路在G1中也有體現,只不其對應的數據是remember set而已。
對于新生代:它記錄老年代到新生代的引用,younggc時不用遍歷整個老年代
對于老年代:它記錄并發標記開始引用發生變化的card,并發標記結束后需要處理這些card
由于新生代GC與老年代GC同時使用card table,所以會出現沖突的情況
Young GC如果發生,比方說:
1、并發標記還未掃描到臟卡1.
2、Young GC掃描完臟卡,并改變dirty到clean.
3、并發標記掃描,發現卡1已不是臟卡,則不會處理,這就造成了漏標。
但這個card必須在remark階段進行重新標記。所以增加了另一個數據結構mod union table解決此問題。
mod union table是一個bit位向量,一個bit表示一個card的狀態。
它由新生代垃圾收集器維護,新生代GC將card設置為clean之前,把mod union table設置為dirty。card table狀態為dirty、或者mod union table標記為dirty、或者同時兩種數據結構都標記為dirty的card表示并發標記階段引用發生了變化,需要在后面的階段進行處理。
write barrie?
write barrie寫屏障類似于一個切面,用戶線程寫對象引用的時候就觸發write barrier的邏輯,將對象所處的card設置為dirty。
并發預清理concurrent preclean
通過參數CMSPrecleaningEnabled選擇關閉該階段,默認啟用,主要做兩件事情:
1、處理新生代已經發現的引用,比如在并發階段,在Eden區中分配了一個A對象,A對象引用了一個老年代對象B(這個B之前沒有被標記),在這個階段就會標記對象B為活躍對象。
2、在并發標記階段,如果老年代中有對象內部引用發生變化,會把所在的Card標記為Dirty(包括ModUnionTalble),通過掃描這些Table,重新標記那些在并發標記階段引用被更新的對象。
處理dirty card,降低remark階段暫停時間。
重新標記的過程是STW的,所以為了縮短停頓時間,在并發標記之前應該盡可能多的完成重新標記階段的工作。并發預清理就是對dirty card進行遍歷處理,降低重新標記需要處理的dirty card的數量。
可中斷預清理concurrent abortable preclean
該階段發生的前提是,新生代Eden區的內存使用量大于參數CMSScheduleRemarkEdenSizeThreshold 默認是2M,如果新生代的對象太少,就沒有必要執行該階段,直接執行重新標記階段。
為什么需要這個階段,存在的價值是什么?
因為CMS GC的終極目標是降低垃圾回收時的暫停時間,所以在該階段要盡最大的努力去處理那些在并發階段被應用線程更新的老年代對象,這樣在暫停的重新標記階段就可以少處理一些,暫停時間也會相應的降低。
在該階段,主要循環的做兩件事:
1、處理 From 和 To 區的對象,標記可達的老年代對象
2、和上一個階段一樣,掃描處理Dirty Card和ModUnionTalble中的對象。
當然了,這個邏輯不會一直循環下去,打斷這個循環的條件有三個:
1、可以設置最多循環的次數 CMSMaxAbortablePrecleanLoops,默認是0,意思沒有循環次數的限制。
2、如果執行這個邏輯的時間達到了閾值CMSMaxAbortablePrecleanTime,默認是5s,會退出循環。
3、如果新生代Eden區的內存使用率達到了閾值CMSScheduleRemarkEdenPenetration,默認50%,會退出循環。
重新標記final remark
在之前的并行階段,可能產生新的引用關系如下:
1、老年代的新對象被GC Roots引用
2、老年代的未標記對象被新生代對象引用
3、老年代已標記的對象增加新引用指向老年代其它對象
4、新生代對象指向老年代引用被刪除
上述對象中可能有一些已經在Precleaning階段和AbortablePreclean階段被處理過,但總存在沒來得及處理的,所以需要做以下事情:
1、遍歷新生代對象,重新標記
2、根據GC Roots,重新標記
3、遍歷老年代的Dirty Card和Mod Union Table,重新標記
在第1步驟中,需要遍歷新生代的全部對象,如果新生代的使用率很高,需要遍歷處理的對象也很多,這對于這個階段的總耗時來說,是個災難(因為可能大量的對象是暫時存活的,而且這些對象也可能引用大量的老年代對象,造成很多應該回收的老年代對象而沒有被回收,遍歷遞歸的次數也增加不少),如果在這之前發生一次YGC,這樣就可以避免掃描無效的對象。
CMS算法中提供了一個參數:CMSScavengeBeforeRemark,默認并沒有開啟,如果開啟該參數,在執行該階段之前,會強制觸發一次YGC,可以減少新生代對象的遍歷時間,回收的也更徹底一點。
并發清除concurrent sweep
并發清除標記為不可達的對象,回收并合并空閑內存。
并發重置concurrent reset
重新設置CMS相關的各種狀態及數據結構,為下一個垃圾收集周期做好準備。
缺點
cpu資源敏感,降低吞吐量
CMS沒有運行的時候所有全部cpu資源都供用戶線程使用,CMS開始并發運行后就要跟用戶線程競爭cpu資源,導致應用線程運行變慢。對于cpu資源非常緊缺的系統,假設只有2核,CMS運行起來后將占用一半的cpu資源,用戶線程將感知到運行速度減半。
并發帶來的好處是可以降低用戶線程的停頓時間,對于在線服務類應用非常有益,因為長時間的停頓可能導致響應超時等問題。但相對于非并發垃圾收集器,CMS整個周期內很多工作是重復的(比如重新標記階段對dirty card中的對象重新標記,而在并發標記階段可能已經標記過了),導致整體的吞吐量是降低的。
浮動垃圾
因為CMS垃圾收集器的特性,被標記過的對象,即使最終變成垃圾本次GC也不會回收它,這些垃圾就是浮動垃圾。浮動垃圾的產生意味著內存里不光裝著存活對象,還要裝著這些浮動垃圾,所以容納同樣多的存活對象CMS需要占用更大的內存空間。
內存碎片
CMS使用標記清除算法,收集結束之后會產生大量內存碎片。當有大對象需要分配空間時,可能總的空間大小是足夠的,但是沒有連續的空間裝下此對象。
CMS默認開啟UseCMSCompactAtFullCollection?參數,在FullGC時進行內存碎片的合并整理。內存碎片雖然解決了,但負面影響就是停頓時間變長了。還有另外一個CMSFullGCsBeforeCompaction參數可以控制多少次FullGC才會進行整理,默認是0代表每次FullGC都會進行碎片整理。
運行過程常見問題
concurrent mode failure
并發雖好,但會引入一些問題。對于非并發的垃圾收集器,可以等到老年代無法分配對象時再執行GC。但對于cms則需要預留出空間提前開始GC,預留的空間供并發期間新對象的分配及新生代對象的晉升使用。如果在老年代分配對象發現老年代裝不下,則會觸發concurrent mode failure,此時將會暫停用戶線程執行FullGC或者串行模式的CMS。
promotion failed
這個錯誤涉及到CMS擔保機制,新生代GC之前會根據歷史晉升到老年代對象的大小,預估本次老年代是否足夠容納新生代晉升的對象。如果預估時空間足夠,但新生代GC實際執行時發現容納不了,則會引起promotion failed錯誤。
OutOfMemoryError?
CMS垃圾收集器發現大部分時間都浪費在GC上就會拋出OutOfMemoryError異常,具體為98%的時間在GC但回收不到2%的空間。這樣做實際上是為了防止程序進入一種雖然在運行實際上一直在GC假死狀態,也可以通過設置-XX:-UseGCOverheadLimit禁用該機制。
?
原文鏈接:https://blog.csdn.net/hongxingxiaonan/article/details/105019325
總結
以上是生活随笔為你收集整理的CMS垃圾收集器详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 对于幸福不是悖论的证明,在现代对于幸福探
- 下一篇: 企鹅龙(DRBL)无盘启动+再生龙(cl