GC算法實現(xiàn)篇之并發(fā)標記清除
Concurrent Mark and Sweep(并發(fā)標記-清除)
CMS的官方名稱為 “Mostly Concurrent Mark and Sweep Garbage Collector”(主要并發(fā)-標記-清除-垃圾收集器). 其對年輕代采用并行 STW方式的 mark-copy (標記-復制)算法, 對老年代主要使用并發(fā) mark-sweep (標記-清除)算法。
CMS的設計目標是避免在老年代垃圾收集時出現(xiàn)長時間的卡頓。主要通過兩種手段來達成此目標。
- 第一, 不對老年代進行整理, 而是使用空閑列表(free-lists)來管理內存空間的回收。
- 第二, 在 mark-and-sweep (標記-清除) 階段的大部分工作和應用線程一起并發(fā)執(zhí)行。
也就是說, 在這些階段并沒有明顯的應用線程暫停。但值得注意的是, 它仍然和應用線程爭搶CPU時間。默認情況下, CMS 使用的并發(fā)線程數(shù)等于CPU內核數(shù)的 1/4。
通過以下選項來指定CMS垃圾收集器:
java -XX:+UseConcMarkSweepGC com.mypackages.MyExecutableClass
如果服務器是多核CPU,并且主要調優(yōu)目標是降低延遲, 那么使用CMS是個很明智的選擇. 減少每一次GC停頓的時間,會直接影響到終端用戶對系統(tǒng)的體驗, 用戶會認為系統(tǒng)非常靈敏。 因為多數(shù)時候都有部分CPU資源被GC消耗, 所以在CPU資源受限的情況下,CMS會比并行GC的吞吐量差一些。
和前面的GC算法一樣, 我們先來看看CMS算法在實際應用中的GC日志, 其中包括一次 minor GC, 以及一次 major GC 停頓:
****-05-26T16:23:07.219-0200:?64.322:?[GC?(Allocation?Failure)?64.322:? ?[ParNew:?613404K->68068K(613440K),?0.1020465?secs] ?10885349K->10880154K(12514816K),?0.1021309?secs] ?[Times:?user=0.78?sys=0.01,?real=0.11?secs] ****-05-26T16:23:07.321-0200:?64.425:?[GC?(CMS?Initial?Mark)? ?[1?CMS-initial-mark:?10812086K(11901376K)]? ?10887844K(12514816K),?0.0001997?secs]? ?[Times:?user=0.00?sys=0.00,?real=0.00?secs] ****-05-26T16:23:07.321-0200:?64.425:?[CMS-concurrent-mark-start] ****-05-26T16:23:07.357-0200:?64.460:?[CMS-concurrent-mark:?0.035/0.035?secs]? ?[Times:?user=0.07?sys=0.00,?real=0.03?secs] ****-05-26T16:23:07.357-0200:?64.460:?[CMS-concurrent-preclean-start] ****-05-26T16:23:07.373-0200:?64.476:?[CMS-concurrent-preclean:?0.016/0.016?secs]? ?[Times:?user=0.02?sys=0.00,?real=0.02?secs] ****-05-26T16:23:07.373-0200:?64.476:?[CMS-concurrent-abortable-preclean-start] ****-05-26T16:23:08.446-0200:?65.550:?[CMS-concurrent-abortable-preclean:?0.167/1.074?secs]? ?[Times:?user=0.20?sys=0.00,?real=1.07?secs] ****-05-26T16:23:08.447-0200:?65.550:?[GC?(CMS?Final?Remark)? ?[YG occupancy:?387920?K?(613440?K)] ?65.550:?[Rescan?(parallel)?,?0.0085125?secs] ?65.559:?[weak refs processing,?0.0000243?secs] ?65.559:?[class?unloading,?0.0013120?secs] ?65.560:?[scrub symbol table,?0.0008345?secs] ?65.561:?[scrub?string?table,?0.0001759?secs] ?[1?CMS-remark:?10812086K(11901376K)]? ?11200006K(12514816K),?0.0110730?secs]? ?[Times:?user=0.06?sys=0.00,?real=0.01?secs] ****-05-26T16:23:08.458-0200:?65.561:?[CMS-concurrent-sweep-start] ****-05-26T16:23:08.485-0200:?65.588:?[CMS-concurrent-sweep:?0.027/0.027?secs]? ?[Times:?user=0.03?sys=0.00,?real=0.03?secs] ****-05-26T16:23:08.485-0200:?65.589:?[CMS-concurrent-reset-start] ****-05-26T16:23:08.497-0200:?65.601:?[CMS-concurrent-reset:?0.012/0.012?secs]? ?[Times:?user=0.01?sys=0.00,?real=0.01?secs]
Minor GC(小型GC)
日志中的第一次GC事件是清理年輕代的小型GC(Minor GC)。讓我們來分析 CMS 垃圾收集器的行為:
****-05-26T16:23:07.219-0200: 64.322:[GC(Allocation Failure) 64.322: [ParNew: 613404K->68068K(613440K), 0.1020465 secs] 10885349K->10880154K(12514816K), 0.1021309 secs] [Times: user=0.78 sys=0.01, real=0.11 secs]
****-05-26T16:23:07.219-0200 – GC事件開始的時間. 其中-0200表示西二時區(qū),而中國所在的東8區(qū)為 +0800。
64.322 – GC事件開始時,相對于JVM啟動時的間隔時間,單位是秒。
GC – 用來區(qū)分 Minor GC 還是 Full GC 的標志。GC表明這是一次小型GC(Minor GC)
Allocation Failure – 觸發(fā)垃圾收集的原因。本次GC事件, 是由于年輕代中沒有適當?shù)目臻g存放新的數(shù)據(jù)結構引起的。
ParNew – 垃圾收集器的名稱。這個名字表示的是在年輕代中使用的: 并行的 標記-復制(mark-copy), 全線暫停(STW)垃圾收集器, 專門設計了用來配合老年代使用的 Concurrent Mark & Sweep 垃圾收集器。
613404K->68068K – 在垃圾收集之前和之后的年輕代使用量。
(613440K) – 年輕代的總大小。
0.1020465 secs – 垃圾收集器在 w/o final cleanup 階段消耗的時間
10885349K->10880154K – 在垃圾收集之前和之后堆內存的使用情況。
(12514816K) – 可用堆的總大小。
0.1021309 secs – 垃圾收集器在標記和復制年輕代存活對象時所消耗的時間。包括和ConcurrentMarkSweep收集器的通信開銷, 提升存活時間達標的對象到老年代,以及垃圾收集后期的一些最終清理。
[Times: user=0.78 sys=0.01, real=0.11 secs] – GC事件的持續(xù)時間, 通過三個部分來衡量:
- user – 在此次垃圾回收過程中, 由GC線程所消耗的總的CPU時間。
- sys – GC過程中中操作系統(tǒng)調用和系統(tǒng)等待事件所消耗的時間。
- real – 應用程序暫停的時間。在并行GC(Parallel GC)中, 這個數(shù)字約等于: (user time + system time)/GC線程數(shù)。 這里使用的是8個線程。 請注意,總是有固定比例的處理過程是不能并行化的。
從上面的日志可以看出,在GC之前總的堆內存使用量為 10,885,349K, 年輕代的使用量為 613,404K。這意味著老年代使用量等于 10,271,945K。GC之后,年輕代的使用量減少了 545,336K, 而總的堆內存使用只下降了 5,195K??梢运愠鲇?nbsp;540,141K 的對象從年輕代提升到老年代。

Full GC(完全GC)
現(xiàn)在, 我們已經(jīng)熟悉了如何解讀GC日志, 接下來將介紹一種完全不同的日志格式。下面這一段很長很長的日志, 就是CMS對老年代進行垃圾收集時輸出的各階段日志。為了簡潔,我們對這些階段逐個介紹。 首先來看CMS收集器整個GC事件的日志:
****-05-26T16:23:07.321-0200:?64.425:?[GC?(CMS?Initial?Mark)? ?[1?CMS-initial-mark:?10812086K(11901376K)]? ?10887844K(12514816K),?0.0001997?secs]? ?[Times:?user=0.00?sys=0.00,?real=0.00?secs] ****-05-26T16:23:07.321-0200:?64.425:?[CMS-concurrent-mark-start] ****-05-26T16:23:07.357-0200:?64.460:?[CMS-concurrent-mark:?0.035/0.035?secs]? ?[Times:?user=0.07?sys=0.00,?real=0.03?secs] ****-05-26T16:23:07.357-0200:?64.460:?[CMS-concurrent-preclean-start] ****-05-26T16:23:07.373-0200:?64.476:?[CMS-concurrent-preclean:?0.016/0.016?secs]? ?[Times:?user=0.02?sys=0.00,?real=0.02?secs] ****-05-26T16:23:07.373-0200:?64.476:?[CMS-concurrent-abortable-preclean-start] ****-05-26T16:23:08.446-0200:?65.550:?[CMS-concurrent-abortable-preclean:?0.167/1.074?secs]? ?[Times:?user=0.20?sys=0.00,?real=1.07?secs] ****-05-26T16:23:08.447-0200:?65.550:?[GC?(CMS?Final?Remark)? ?[YG occupancy:?387920?K?(613440?K)] ?65.550:?[Rescan?(parallel)?,?0.0085125?secs] ?65.559:?[weak refs processing,?0.0000243?secs] ?65.559:?[class?unloading,?0.0013120?secs] ?65.560:?[scrub symbol table,?0.0008345?secs] ?65.561:?[scrub?string?table,?0.0001759?secs] ?[1?CMS-remark:?10812086K(11901376K)]? ?11200006K(12514816K),?0.0110730?secs]? ?[Times:?user=0.06?sys=0.00,?real=0.01?secs] ****-05-26T16:23:08.458-0200:?65.561:?[CMS-concurrent-sweep-start] ****-05-26T16:23:08.485-0200:?65.588:?[CMS-concurrent-sweep:?0.027/0.027?secs]? ?[Times:?user=0.03?sys=0.00,?real=0.03?secs] ****-05-26T16:23:08.485-0200:?65.589:?[CMS-concurrent-reset-start] ****-05-26T16:23:08.497-0200:?65.601:?[CMS-concurrent-reset:?0.012/0.012?secs]? ?[Times:?user=0.01?sys=0.00,?real=0.01?secs]
只是要記住 —— 在實際情況下, 進行老年代的并發(fā)回收時, 可能會伴隨著多次年輕代的小型GC. 在這種情況下, 大型GC的日志中就會摻雜著多次小型GC事件, 像前面所介紹的一樣。
階段 1: Initial Mark(初始標記). 這是第一次STW事件。 此階段的目標是標記老年代中所有存活的對象, 包括 GC ROOR 的直接引用, 以及由年輕代中存活對象所引用的對象。 后者也非常重要, 因為老年代是獨立進行回收的。
****-05-26T16:23:07.321-0200: 64.421: [GC (CMS Initial Mark1 [1 CMS-initial-mark: 10812086K1(11901376K)1] 10887844K1(12514816K)1, 0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]1
****-05-26T16:23:07.321-0200: 64.42 – GC事件開始的時間. 其中 -0200 是時區(qū),而中國所在的東8區(qū)為 +0800。 而 64.42 是相對于JVM啟動的時間。 下面的其他階段也是一樣,所以就不再重復介紹。
CMS Initial Mark – 垃圾回收的階段名稱為 “Initial Mark”。 標記所有的 GC Root。
10812086K – 老年代的當前使用量。
(11901376K) – 老年代中可用內存總量。
10887844K – 當前堆內存的使用量。
(12514816K) – 可用堆的總大小。
0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] – 此次暫停的持續(xù)時間, 以 user, system 和 real time 3個部分進行衡量。
階段 2: Concurrent Mark(并發(fā)標記). 在此階段, 垃圾收集器遍歷老年代, 標記所有的存活對象, 從前一階段 “Initial Mark” 找到的 root 根開始算起。 顧名思義, “并發(fā)標記”階段, 就是與應用程序同時運行,不用暫停的階段。 請注意, 并非所有老年代中存活的對象都在此階段被標記, 因為在標記過程中對象的引用關系還在發(fā)生變化。

在上面的示意圖中, “Current object” 旁邊的一個引用被標記線程并發(fā)刪除了。
****-05-26T16:23:07.321-0200: 64.425: [CMS-concurrent-mark-start] ****-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-mark1: 035/0.035 secs1] [Times: user=0.07 sys=0.00, real=0.03 secs]1
CMS-concurrent-mark – 并發(fā)標記(“Concurrent Mark”) 是CMS垃圾收集中的一個階段, 遍歷老年代并標記所有的存活對象。
035/0.035 secs – 此階段的持續(xù)時間, 分別是運行時間和相應的實際時間。
[Times: user=0.07 sys=0.00, real=0.03 secs] – Times 這部分對并發(fā)階段來說沒多少意義, 因為是從并發(fā)標記開始時計算的,而這段時間內不僅并發(fā)標記在運行,程序也在運行
階段 3: Concurrent Preclean(并發(fā)預清理). 此階段同樣是與應用線程并行執(zhí)行的, 不需要停止應用線程。 因為前一階段是與程序并發(fā)進行的,可能有一些引用已經(jīng)改變。如果在并發(fā)標記過程中發(fā)生了引用關系變化,JVM會(通過“Card”)將發(fā)生了改變的區(qū)域標記為“臟”區(qū)(這就是所謂的卡片標記,Card Marking)。

在預清理階段,這些臟對象會被統(tǒng)計出來,從他們可達的對象也被標記下來。此階段完成后, 用以標記的 card 也就被清空了。

此外, 本階段也會執(zhí)行一些必要的細節(jié)處理, 并為 Final Remark 階段做一些準備工作。
****-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start] ****-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-preclean: 0.016/0.016 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
CMS-concurrent-preclean – 并發(fā)預清理階段, 統(tǒng)計此前的標記階段中發(fā)生了改變的對象。
0.016/0.016 secs – 此階段的持續(xù)時間, 分別是運行時間和對應的實際時間。
[Times: user=0.02 sys=0.00, real=0.02 secs] – Times 這部分對并發(fā)階段來說沒多少意義, 因為是從并發(fā)標記開始時計算的,而這段時間內不僅GC的并發(fā)標記在運行,程序也在運行。
階段 4: Concurrent Abortable Preclean(并發(fā)可取消的預清理). 此階段也不停止應用線程. 本階段嘗試在 STW 的 Final Remark 之前盡可能地多做一些工作。本階段的具體時間取決于多種因素, 因為它循環(huán)做同樣的事情,直到滿足某個退出條件( 如迭代次數(shù), 有用工作量, 消耗的系統(tǒng)時間,等等)。
****-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-abortable-preclean-start] ****-05-26T16:23:08.446-0200: 65.550: [CMS-concurrent-abortable-preclean1: 0.167/1.074 secs2] [Times: user=0.20 sys=0.00, real=1.07 secs]3
CMS-concurrent-abortable-preclean – 此階段的名稱: “Concurrent Abortable Preclean”。
0.167/1.074 secs – 此階段的持續(xù)時間, 運行時間和對應的實際時間。有趣的是, 用戶時間明顯比時鐘時間要小很多。通常情況下我們看到的都是時鐘時間小于用戶時間, 這意味著因為有一些并行工作, 所以運行時間才會小于使用的CPU時間。這里只進行了少量的工作 — 0.167秒的CPU時間,GC線程經(jīng)歷了很多系統(tǒng)等待。從本質上講,GC線程試圖在必須執(zhí)行 STW暫停之前等待盡可能長的時間。默認條件下,此階段可以持續(xù)最多5秒鐘。
[Times: user=0.20 sys=0.00, real=1.07 secs] – “Times” 這部分對并發(fā)階段來說沒多少意義, 因為是從并發(fā)標記開始時計算的,而這段時間內不僅GC的并發(fā)標記線程在運行,程序也在運行
此階段可能顯著影響STW停頓的持續(xù)時間, 并且有許多重要的配置選項和失敗模式。
階段 5: Final Remark(最終標記). 這是此次GC事件中第二次(也是最后一次)STW階段。本階段的目標是完成老年代中所有存活對象的標記. 因為之前的 preclean 階段是并發(fā)的, 有可能無法跟上應用程序的變化速度。所以需要 STW暫停來處理復雜情況。
通常CMS會嘗試在年輕代盡可能空的情況運行 final remark 階段, 以免接連多次發(fā)生 STW 事件。
看起來稍微比之前的階段要復雜一些:
****-05-26T16:23:08.447-0200: 65.550: [GC (CMS Final Remark) [YG occupancy: 387920 K (613440 K)] 65.550: [Rescan (parallel) , 0.0085125 secs] 65.559: [weak refs processing, 0.0000243 secs] 65.559: [class unloading, 0.0013120 secs] 65.560: [scrub string table, 0.0001759 secs] [1 CMS-remark: 10812086K(11901376K)] 11200006K(12514816K),0.0110730 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
****-05-26T16:23:08.447-0200: 65.550 – GC事件開始的時間. 包括時鐘時間,以及相對于JVM啟動的時間. 其中-0200表示西二時區(qū),而中國所在的東8區(qū)為 +0800。
CMS Final Remark – 此階段的名稱為 “Final Remark”, 標記老年代中所有存活的對象,包括在此前的并發(fā)標記過程中創(chuàng)建/修改的引用。
YG occupancy: 387920 K (613440 K) – 當前年輕代的使用量和總容量。
[Rescan (parallel) , 0.0085125 secs] – 在程序暫停時重新進行掃描(Rescan),以完成存活對象的標記。此時 rescan 是并行執(zhí)行的,消耗的時間為 0.0085125秒。
weak refs processing, 0.0000243 secs]65.559 – 處理弱引用的第一個子階段(sub-phases)。 顯示的是持續(xù)時間和開始時間戳。
class unloading, 0.0013120 secs]65.560 – 第二個子階段, 卸載不使用的類。 顯示的是持續(xù)時間和開始的時間戳。
scrub string table, 0.0001759 secs – 最后一個子階段, 清理持有class級別 metadata 的符號表(symbol tables),以及內部化字符串對應的 string tables。當然也顯示了暫停的時鐘時間。
10812086K(11901376K) – 此階段完成后老年代的使用量和總容量
11200006K(12514816K) – 此階段完成后整個堆內存的使用量和總容量
0.0110730 secs – 此階段的持續(xù)時間。
[Times: user=0.06 sys=0.00, real=0.01 secs] – GC事件的持續(xù)時間, 通過不同的類別來衡量: user, system and real time。
在5個標記階段完成之后, 老年代中所有的存活對象都被標記了, 現(xiàn)在GC將清除所有不使用的對象來回收老年代空間:
階段 6: Concurrent Sweep(并發(fā)清除). 此階段與應用程序并發(fā)執(zhí)行,不需要STW停頓。目的是刪除未使用的對象,并收回他們占用的空間。

****-05-26T16:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start] ****-05-26T16:23:08.485-0200: 65.588: [CMS-concurrent-sweep: 0.027/0.027 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
CMS-concurrent-sweep – 此階段的名稱, “Concurrent Sweep”, 清除未被標記、不再使用的對象以釋放內存空間。
0.027/0.027 secs – 此階段的持續(xù)時間, 分別是運行時間和實際時間
[Times: user=0.03 sys=0.00, real=0.03 secs] – “Times”部分對并發(fā)階段來說沒有多少意義, 因為是從并發(fā)標記開始時計算的,而這段時間內不僅是并發(fā)標記在運行,程序也在運行。
階段 7: Concurrent Reset(并發(fā)重置). 此階段與應用程序并發(fā)執(zhí)行,重置CMS算法相關的內部數(shù)據(jù), 為下一次GC循環(huán)做準備。
****-05-26T16:23:08.485-0200: 65.589: [CMS-concurrent-reset-start] ****-05-26T16:23:08.497-0200: 65.601: [CMS-concurrent-reset: 0.012/0.012 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
CMS-concurrent-reset – 此階段的名稱, “Concurrent Reset”, 重置CMS算法的內部數(shù)據(jù)結構, 為下一次GC循環(huán)做準備。
0.012/0.012 secs – 此階段的持續(xù)時間, 分別是運行時間和對應的實際時間
[Times: user=0.01 sys=0.00, real=0.01 secs] – “Times”部分對并發(fā)階段來說沒多少意義, 因為是從并發(fā)標記開始時計算的,而這段時間內不僅GC線程在運行,程序也在運行。
總之, CMS垃圾收集器在減少停頓時間上做了很多給力的工作, 大量的并發(fā)線程執(zhí)行的工作并不需要暫停應用線程。 當然, CMS也有一些缺點,其中最大的問題就是老年代內存碎片問題, 在某些情況下GC會造成不可預測的暫停時間, 特別是堆內存較大的情況下。
原文鏈接:https://plumbr.io/handbook/garbage-collection-algorithms-implementations#concurrent-mark-and-sweep
以上就是GC算法實現(xiàn)篇之并發(fā)標記清除的詳細內容,更多關于GC算法并發(fā)標記清除的資料請關注腳本之家其它相關文章!
相關文章
論java如何通過反射獲得方法真實參數(shù)名及擴展研究
這篇文章主要為大家介紹了java如何通過反射獲得方法的真實參數(shù)名以及擴展研究,有需要的朋友可以借鑒參考下,希望能夠有所幫助祝大家多多進步早日升職加薪2022-01-01
SpringBoot + Spring Cloud Consul 服務注冊和發(fā)現(xiàn)詳細解析
這篇文章主要介紹了SpringBoot + Spring Cloud Consul 服務注冊和發(fā)現(xiàn),本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07
Java中Object.equals和String.equals的區(qū)別詳解
這篇文章主要給大家介紹了Java中Object.equals和String.equals的區(qū)別,文中通過一個小示例讓大家輕松的明白這兩者的區(qū)別,對大家具有一定的參考價值,需要的朋友們下面來一起看看吧。2017-04-04
Java實現(xiàn)單鏈表SingleLinkedList增刪改查及反轉 逆序等
單鏈表是鏈表的其中一種基本結構。一個最簡單的結點結構如圖所示,它是構成單鏈表的基本結點結構。在結點中數(shù)據(jù)域用來存儲數(shù)據(jù)元素,指針域用于指向下一個具有相同結構的結點。 因為只有一個指針結點,稱為單鏈表2021-10-10
Spring中的DefaultResourceLoader使用方法解讀
這篇文章主要介紹了Spring中的DefaultResourceLoader使用方法解讀,DefaultResourceLoader是spring提供的一個默認的資源加載器,DefaultResourceLoader實現(xiàn)了ResourceLoader接口,提供了基本的資源加載能力,需要的朋友可以參考下2024-02-02
java使用MulticastSocket實現(xiàn)組播
這篇文章主要為大家詳細介紹了java使用MulticastSocket實現(xiàn)組播,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-01-01

