《GC篇》四、GC 算法(實現篇)

4. GC 算法(實現篇)

學習了GC算法的相關概念之後, 我們將介紹在JVM中這些算法的具體實現。首先要記住的是, 大多數JVM都需要使用兩種不同的GC算法 —— 一種用來清理年輕代, 另一種用來清理老年代。

我們可以選擇JVM內置的各種算法。如果不通過參數明確指定垃圾收集算法, 則會使用宿主平臺的默認實現。本章會詳細介紹各種算法的實現原理。

下面是關於Java 8中各種組合的垃圾收集器概要列表,對於之前的Java版本來說,可用組合會有一些不同:

Young Tenured JVM options
Incremental(增量GC) Incremental -Xincgc
Serial Serial -XX:+UseSerialGC
Parallel Scavenge Serial -XX:+UseParallelGC -XX:-UseParallelOldGC
Parallel New Serial N/A
Serial Parallel Old N/A
Parallel Scavenge Parallel Old -XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel New Parallel Old N/A
Serial CMS -XX:-UseParNewGC -XX:+UseConcMarkSweepGC
Parallel Scavenge CMS N/A
Parallel New CMS -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
G1 -XX:+UseG1GC

看起來有些複雜, 不用擔心。主要使用的是上表中黑體字表示的這四種組合。其餘的要麼是被廢棄(deprecated), 要麼是不支持或者是不太適用於生產環境。所以,接下來我們只介紹下面這些組合及其工作原理:

  • 年輕代和老年代的串行GC(Serial GC)
  • 年輕代和老年代的並行GC(Parallel GC)
  • 年輕代的並行GC(Parallel New) + 老年代的CMS(Concurrent Mark and Sweep)
  • G1, 負責回收年輕代和老年代

Serial GC(串行GC)

Serial GC 對年輕代使用 mark-copy(標記-複製) 算法, 對老年代使用 mark-sweep-compact(標記-清除-整理)算法. 顧名思義, 兩者都是單線程的垃圾收集器,不能進行並行處理。兩者都會觸發全線暫停(STW),停止所有的應用線程。

因此這種GC算法不能充分利用多核CPU。不管有多少CPU內核, JVM 在垃圾收集時都只能使用單個核心。

要啓用此款收集器, 只需要指定一個JVM啓動參數即可,同時對年輕代和老年代生效:

java -XX:+UseSerialGC com.mypackages.MyExecutableClass

該選項只適合幾百MB堆內存的JVM,而且是單核CPU時比較有用。 對於服務器端來說, 因爲一般是多個CPU內核, 並不推薦使用, 除非確實需要限制JVM所使用的資源。大多數服務器端應用部署在多核平臺上, 選擇 Serial GC 就表示人爲的限制系統資源的使用。 導致的就是資源閒置, 多的CPU資源也不能用來降低延遲,也不能用來增加吞吐量。

下面讓我們看看Serial GC的垃圾收集日誌, 並從中提取什麼有用的信息。爲了打開GC日誌記錄, 我們使用下面的JVM啓動參數:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps 
-XX:+PrintGCTimeStamps

產生的GC日誌輸出類似這樣(爲了排版,已手工折行):

2015-05-26T14:45:37.987-0200: 
        151.126: [GC (Allocation Failure) 
        151.126: [DefNew: 629119K->69888K(629120K), 0.0584157 secs] 
        1619346K->1273247K(2027264K), 0.0585007 secs]
    [Times: user=0.06 sys=0.00, real=0.06 secs]
2015-05-26T14:45:59.690-0200: 
        172.829: [GC (Allocation Failure) 
        172.829: [DefNew: 629120K->629120K(629120K), 0.0000372 secs]
        172.829: [Tenured: 1203359K->755802K(1398144K), 0.1855567 secs] 
        1832479K->755802K(2027264K), 
        [Metaspace: 6741K->6741K(1056768K)], 0.1856954 secs] 
    [Times: user=0.18 sys=0.00, real=0.18 secs]

此GC日誌片段展示了在JVM中發生的很多事情。 實際上,在這段日誌中產生了兩個GC事件, 其中一次清理的是年輕代,另一次清理的是整個堆內存。讓我們先來分析前一次GC,其在年輕代中產生。

Minor GC(小型GC)

以下代碼片段展示了清理年輕代內存的GC事件:

2015-05-26T14:45:37.987-02001 : 151.12622 : [ GC3 (Allocation Failure4 151.126:

[DefNew5 : 629119K->69888K6 (629120K)7 , 0.0584157 secs] 1619346K->1273247K8

(2027264K)9, 0.0585007 secs10] [Times: user=0.06 sys=0.00, real=0.06 secs]11


  1. 2015-05-26T14:45:37.987-0200 – GC事件開始的時間. 其中-0200表示西二時區,而中國所在的東8區爲 +0800

  2. 151.126 – GC事件開始時,相對於JVM啓動時的間隔時間,單位是秒。

  3. GC – 用來區分 Minor GC 還是 Full GC 的標誌。GC表明這是一次小型GC(Minor GC)

  4. Allocation Failure – 觸發 GC 的原因。本次GC事件, 是由於年輕代中沒有空間來存放新的數據結構引起的。

  5. DefNew – 垃圾收集器的名稱。這個神祕的名字表示的是在年輕代中使用的: 單線程, 標記-複製(mark-copy), 全線暫停(STW) 垃圾收集器。

  6. 629119K->69888K – 在垃圾收集之前和之後年輕代的使用量。

  7. (629120K) – 年輕代總的空間大小。

  8. 1619346K->1273247K – 在垃圾收集之前和之後整個堆內存的使用情況。

  9. (2027264K) – 可用堆的總空間大小。

  10. 0.0585007 secs – GC事件持續的時間,以秒爲單位。

  11. [Times: user=0.06 sys=0.00, real=0.06 secs] – GC事件的持續時間, 通過三個部分來衡量:

  • user – 在此次垃圾回收過程中, 所有 GC線程所消耗的CPU時間之和。
  • sys – GC過程中中操作系統調用和系統等待事件所消耗的時間。

  • real – 應用程序暫停的時間。因爲串行垃圾收集器(Serial Garbage Collector)只使用單線程, 因此 real time 等於 user 和 system 時間的總和。

可以從上面的日誌片段瞭解到, 在GC事件中,JVM 的內存使用情況發生了怎樣的變化。此次垃圾收集之前, 堆內存總的使用量爲 1,619,346K。其中,年輕代使用了 629,119K。可以算出,老年代使用量爲 990,227K

更重要的信息蘊含在下一批數字中, 垃圾收集之後, 年輕代的使用量減少了 559,231K, 但堆內存的總體使用量只下降了 346,099K。 從中可以算出,有 213,132K 的對象從年輕代提升到了老年代。

此次GC事件也可以用下面的示意圖來說明, 顯示的是GC開始之前, 以及剛剛結束之後, 這兩個時間點內存使用情況的快照:

4. GC 算法(實現篇) - 圖1

Full GC(完全GC)

理解第一次 minor GC 事件後,讓我們看看日誌中的第二次GC事件:

2015-05-26T14:45:59.690-02001 : 172.8292 : [GC (Allocation Failure 172.829:

[DefNew: 629120K->629120K(629120K), 0.0000372 secs3] 172.829:[Tenured4:

1203359K->755802K5 (1398144K)6, 0.1855567 secs7 ] 1832479K->755802K8

(2027264K)9, [Metaspace: 6741K->6741K(1056768K)]10

[Times: user=0.18 sys=0.00, real=0.18 secs]11


  1. 2015-05-26T14:45:59.690-0200 – GC事件開始的時間. 其中-0200表示西二時區,而中國所在的東8區爲 +0800
  2. 172.829 – GC事件開始時,相對於JVM啓動時的間隔時間,單位是秒。
  3. [DefNew: 629120K->629120K(629120K), 0.0000372 secs – 與上面的示例類似, 因爲內存分配失敗,在年輕代中發生了一次 minor GC。此次GC同樣使用的是 DefNew 收集器, 讓年輕代的使用量從 629120K 降爲 0。注意,JVM對此次GC的報告有些問題,誤將年輕代認爲是完全填滿的。此次垃圾收集消耗了 0.0000372秒。

  4. Tenured – 用於清理老年代空間的垃圾收集器名稱。Tenured 表明使用的是單線程的全線暫停垃圾收集器, 收集算法爲 標記-清除-整理(mark-sweep-compact )。

  5. 1203359K->755802K – 在垃圾收集之前和之後老年代的使用量。
  6. (1398144K) – 老年代的總空間大小。
  7. 0.1855567 secs – 清理老年代所花的時間。
  8. 1832479K->755802K – 在垃圾收集之前和之後,整個堆內存的使用情況。
  9. (2027264K) – 可用堆的總空間大小。
  10. [Metaspace: 6741K->6741K(1056768K)] – 關於 Metaspace 空間, 同樣的信息。可以看出, 此次GC過程中 Metaspace 中沒有收集到任何垃圾。
  11. [Times: user=0.18 sys=0.00, real=0.18 secs] – GC事件的持續時間, 通過三個部分來衡量:
  • user – 在此次垃圾回收過程中, 所有 GC線程所消耗的CPU時間之和。
  • sys – GC過程中中操作系統調用和系統等待事件所消耗的時間。
  • real – 應用程序暫停的時間。因爲串行垃圾收集器(Serial Garbage Collector)只使用單線程, 因此 real time 等於 user 和 system 時間的總和。

和 Minor GC 相比,最明顯的區別是 —— 在此次GC事件中, 除了年輕代, 還清理了老年代和Metaspace. 在GC事件開始之前, 以及剛剛結束之後的內存佈局,可以用下面的示意圖來說明:

4. GC 算法(實現篇) - 圖2

Parallel GC(並行GC)

並行垃圾收集器這一類組合, 在年輕代使用 標記-複製(mark-copy)算法, 在老年代使用 標記-清除-整理(mark-sweep-compact)算法。年輕代和老年代的垃圾回收都會觸發STW事件,暫停所有的應用線程來執行垃圾收集。兩者在執行 標記和 複製/整理階段時都使用多個線程, 因此得名“(Parallel)”。通過並行執行, 使得GC時間大幅減少。

通過命令行參數 -XX:ParallelGCThreads=NNN 來指定 GC 線程數。 其默認值爲CPU內核數。

可以通過下面的任意一組命令行參數來指定並行GC:

在這裏插入代碼片

並行垃圾收集器適用於多核服務器,主要目標是增加吞吐量。因爲對系統資源的有效使用,能達到更高的吞吐量:

  • 在GC期間, 所有 CPU 內核都在並行清理垃圾, 所以暫停時間更短
  • 在兩次GC週期的間隔期, 沒有GC線程在運行,不會消耗任何系統資源

另一方面, 因爲此GC的所有階段都不能中斷, 所以並行GC很容易出現長時間的卡頓. 如果延遲是系統的主要目標, 那麼就應該選擇其他垃圾收集器組合。

譯者注: 長時間卡頓的意思是,此GC啓動之後,屬於一次性完成所有操作, 於是單次 pause 的時間會較長。

讓我們看看並行垃圾收集器的GC日誌長什麼樣, 從中我們可以得到哪些有用信息。下面的GC日誌中顯示了一次 minor GC 暫停 和一次 major GC 暫停:

2015-05-26T14:27:40.915-0200: 116.115: [GC (Allocation Failure) 
        [PSYoungGen: 2694440K->1305132K(2796544K)] 
    9556775K->8438926K(11185152K)
    , 0.2406675 secs] 
    [Times: user=1.77 sys=0.01, real=0.24 secs]
2015-05-26T14:27:41.155-0200: 116.356: [Full GC (Ergonomics) 
        [PSYoungGen: 1305132K->0K(2796544K)] 
        [ParOldGen: 7133794K->6597672K(8388608K)] 8438926K->6597672K(11185152K), 
        [Metaspace: 6745K->6745K(1056768K)]
    , 0.9158801 secs]
    [Times: user=4.49 sys=0.64, real=0.92 secs]

Minor GC(小型GC)

第一次GC事件表示發生在年輕代的垃圾收集:

2015-05-26T14:27:40.915-02001: 116.1152: [ GC3 (Allocation Failure4)

[PSYoungGen5: 2694440K->1305132K6 (2796544K)7] 9556775K->8438926K8

(11185152K)9, 0.2406675 secs10]

[Times: user=1.77 sys=0.01, real=0.24 secs]11


  1. 2015-05-26T14:27:40.915-0200 – GC事件開始的時間. 其中-0200表示西二時區,而中國所在的東8區爲 +0800
  2. 116.115 – GC事件開始時,相對於JVM啓動時的間隔時間,單位是秒。
  3. GC – 用來區分 Minor GC 還是 Full GC 的標誌。GC表明這是一次小型GC(Minor GC)
  4. Allocation Failure – 觸發垃圾收集的原因。本次GC事件, 是由於年輕代中沒有適當的空間存放新的數據結構引起的。
  5. PSYoungGen – 垃圾收集器的名稱。這個名字表示的是在年輕代中使用的: 並行的 標記-複製(mark-copy), 全線暫停(STW) 垃圾收集器。
  6. 2694440K->1305132K – 在垃圾收集之前和之後的年輕代使用量。
  7. (2796544K) – 年輕代的總大小。
  8. 9556775K->8438926K – 在垃圾收集之前和之後整個堆內存的使用量。
  9. (11185152K) – 可用堆的總大小。
  10. 0.2406675 secs – GC事件持續的時間,以秒爲單位。
  11. [Times: user=1.77 sys=0.01, real=0.24 secs] – GC事件的持續時間, 通過三個部分來衡量:
  • user – 在此次垃圾回收過程中, 由GC線程所消耗的總的CPU時間。
  • sys – GC過程中中操作系統調用和系統等待事件所消耗的時間。
  • real – 應用程序暫停的時間。在 Parallel GC 中, 這個數字約等於: (user time + system time)/GC線程數。 這裏使用了8個線程。 請注意,總有一定比例的處理過程是不能並行進行的。

所以,可以簡單地算出, 在垃圾收集之前, 堆內存總使用量爲 9,556,775K。 其中年輕代爲 2,694,440K。同時算出老年代使用量爲 6,862,335K. 在垃圾收集之後, 年輕代使用量減少爲 1,389,308K, 但總的堆內存使用量只減少了 1,117,849K。這表示有大小爲 271,459K 的對象從年輕代提升到老年代。

4. GC 算法(實現篇) - 圖3

Full GC(完全GC)

學習了並行GC如何清理年輕代之後, 下面介紹清理整個堆內存的GC日誌以及如何進行分析:

2015-05-26T14:27:41.155-0200 : 116.356 : [Full GC (Ergonomics)

[PSYoungGen: 1305132K->0K(2796544K)] [ParOldGen :7133794K->6597672K

(8388608K)] 8438926K->6597672K (11185152K),

[Metaspace: 6745K->6745K(1056768K)], 0.9158801 secs,

[Times: user=4.49 sys=0.64, real=0.92 secs]


  1. 2015-05-26T14:27:41.155-0200 – GC事件開始的時間. 其中-0200表示西二時區,而中國所在的東8區爲 +0800
  2. 116.356 – GC事件開始時,相對於JVM啓動時的間隔時間,單位是秒。 我們可以看到, 此次事件在前一次 MinorGC完成之後立刻就開始了。
  3. Full GC – 用來表示此次是 Full GC 的標誌。Full GC表明本次清理的是年輕代和老年代。
  4. Ergonomics – 觸發垃圾收集的原因。Ergonomics 表示JVM內部環境認爲此時可以進行一次垃圾收集。
  5. [PSYoungGen: 1305132K->0K(2796544K)] – 和上面的示例一樣, 清理年輕代的垃圾收集器是名爲 “PSYoungGen” 的STW收集器, 採用標記-複製(mark-copy)算法。 年輕代使用量從 1305132K 變爲 0, 一般 Full GC 的結果都是這樣。
  6. ParOldGen – 用於清理老年代空間的垃圾收集器類型。在這裏使用的是名爲 ParOldGen 的垃圾收集器, 這是一款並行 STW垃圾收集器, 算法爲 標記-清除-整理(mark-sweep-compact)。
  7. 7133794K->6597672K – 在垃圾收集之前和之後老年代內存的使用情況。
  8. (8388608K) – 老年代的總空間大小。
  9. 8438926K->6597672K – 在垃圾收集之前和之後堆內存的使用情況。
  10. (11185152K) – 可用堆內存的總容量。
  11. [Metaspace: 6745K->6745K(1056768K)] – 類似的信息,關於 Metaspace 空間的。可以看出, 在GC事件中 Metaspace 裏面沒有回收任何對象。
  12. 0.9158801 secs – GC事件持續的時間,以秒爲單位。
  13. [Times: user=4.49 sys=0.64, real=0.92 secs] – GC事件的持續時間, 通過三個部分來衡量:
  • user – 在此次垃圾回收過程中, 由GC線程所消耗的總的CPU時間。
  • sys – GC過程中中操作系統調用和系統等待事件所消耗的時間。
  • real – 應用程序暫停的時間。在 Parallel GC 中, 這個數字約等於: (user time + system time)/GC線程數。 這裏使用了8個線程。 請注意,總有一定比例的處理過程是不能並行進行的。

同樣,和 Minor GC 的區別是很明顯的 —— 在此次GC事件中, 除了年輕代, 還清理了老年代和 Metaspace. 在GC事件前後的內存佈局如下圖所示:

4. GC 算法(實現篇) - 圖4

Concurrent Mark and Sweep(併發標記-清除)

CMS的官方名稱爲 “Mostly Concurrent Mark and Sweep Garbage Collector”(主要併發-標記-清除-垃圾收集器). 其對年輕代採用並行 STW方式的 mark-copy (標記-複製)算法, 對老年代主要使用併發 mark-sweep (標記-清除)算法

CMS的設計目標是避免在老年代垃圾收集時出現長時間的卡頓。主要通過兩種手段來達成此目標。

  • 第一, 不對老年代進行整理, 而是使用空閒列表(free-lists)來管理內存空間的回收。
  • 第二, 在 mark-and-sweep (標記-清除) 階段的大部分工作和應用線程一起併發執行。

也就是說, 在這些階段並沒有明顯的應用線程暫停。但值得注意的是, 它仍然和應用線程爭搶CPU時間。默認情況下, CMS 使用的併發線程數等於CPU內核數的 1/4

通過以下選項來指定CMS垃圾收集器:

java -XX:+UseConcMarkSweepGC com.mypackages.MyExecutableClass

如果服務器是多核CPU,並且主要調優目標是降低延遲, 那麼使用CMS是個很明智的選擇. 減少每一次GC停頓的時間,會直接影響到終端用戶對系統的體驗, 用戶會認爲系統非常靈敏。 因爲多數時候都有部分CPU資源被GC消耗, 所以在CPU資源受限的情況下,CMS會比並行GC的吞吐量差一些。

和前面的GC算法一樣, 我們先來看看CMS算法在實際應用中的GC日誌, 其中包括一次 minor GC, 以及一次 major GC 停頓:

2015-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]
2015-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]
2015-05-26T16:23:07.321-0200: 64.425: [CMS-concurrent-mark-start]
2015-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]
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start]
2015-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]
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-abortable-preclean-start]
2015-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]
2015-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]
2015-05-26T16:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start]
2015-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]
2015-05-26T16:23:08.485-0200: 65.589: [CMS-concurrent-reset-start]
2015-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 垃圾收集器的行爲:

2015-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]


  1. 2015-05-26T16:23:07.219-0200 – GC事件開始的時間. 其中-0200表示西二時區,而中國所在的東8區爲 +0800
  2. 64.322 – GC事件開始時,相對於JVM啓動時的間隔時間,單位是秒。
  3. GC – 用來區分 Minor GC 還是 Full GC 的標誌。GC表明這是一次小型GC(Minor GC)
  4. Allocation Failure – 觸發垃圾收集的原因。本次GC事件, 是由於年輕代中沒有適當的空間存放新的數據結構引起的。
  5. ParNew – 垃圾收集器的名稱。這個名字表示的是在年輕代中使用的: 並行的 標記-複製(mark-copy), 全線暫停(STW)垃圾收集器, 專門設計了用來配合老年代使用的 Concurrent Mark & Sweep 垃圾收集器。
  6. 613404K->68068K – 在垃圾收集之前和之後的年輕代使用量。
  7. (613440K) – 年輕代的總大小。
  8. 0.1020465 secs – 垃圾收集器在 w/o final cleanup 階段消耗的時間
  9. 10885349K->10880154K – 在垃圾收集之前和之後堆內存的使用情況。
  10. (12514816K) – 可用堆的總大小。
  11. 0.1021309 secs – 垃圾收集器在標記和複製年輕代存活對象時所消耗的時間。包括和ConcurrentMarkSweep收集器的通信開銷, 提升存活時間達標的對象到老年代,以及垃圾收集後期的一些最終清理。
  12. [Times: user=0.78 sys=0.01, real=0.11 secs] – GC事件的持續時間, 通過三個部分來衡量:
  • user – 在此次垃圾回收過程中, 由GC線程所消耗的總的CPU時間。
  • sys – GC過程中中操作系統調用和系統等待事件所消耗的時間。
  • real – 應用程序暫停的時間。在並行GC(Parallel GC)中, 這個數字約等於: (user time + system time)/GC線程數。 這裏使用的是8個線程。 請注意,總是有固定比例的處理過程是不能並行化的。

從上面的日誌可以看出,在GC之前總的堆內存使用量爲 10,885,349K, 年輕代的使用量爲 613,404K。這意味着老年代使用量等於 10,271,945K。GC之後,年輕代的使用量減少了 545,336K, 而總的堆內存使用只下降了 5,195K。可以算出有 540,141K 的對象從年輕代提升到老年代。

4. GC 算法(實現篇) - 圖5

Full GC(完全GC)

現在, 我們已經熟悉瞭如何解讀GC日誌, 接下來將介紹一種完全不同的日誌格式。下面這一段很長很長的日誌, 就是CMS對老年代進行垃圾收集時輸出的各階段日誌。爲了簡潔,我們對這些階段逐個介紹。 首先來看CMS收集器整個GC事件的日誌:

2015-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]
2015-05-26T16:23:07.321-0200: 64.425: [CMS-concurrent-mark-start]
2015-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]
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start]
2015-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]
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-abortable-preclean-start]
2015-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]
2015-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]
2015-05-26T16:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start]
2015-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]
2015-05-26T16:23:08.485-0200: 65.589: [CMS-concurrent-reset-start]
2015-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]

只是要記住 —— 在實際情況下, 進行老年代的併發回收時, 可能會伴隨着多次年輕代的小型GC. 在這種情況下, 大型GC的日誌中就會摻雜着多次小型GC事件, 像前面所介紹的一樣。

階段 1: Initial Mark(初始標記). 這是第一次STW事件。 此階段的目標是標記老年代中所有存活的對象, 包括 GC ROOR 的直接引用, 以及由年輕代中存活對象所引用的對象。 後者也非常重要, 因爲老年代是獨立進行回收的。

4. GC 算法(實現篇) - 圖6

2015-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


  1. 2015-05-26T16:23:07.321-0200: 64.42 – GC事件開始的時間. 其中 -0200 是時區,而中國所在的東8區爲 +0800。 而 64.42 是相對於JVM啓動的時間。 下面的其他階段也是一樣,所以就不再重複介紹。
  2. CMS Initial Mark – 垃圾回收的階段名稱爲 “Initial Mark”。 標記所有的 GC Root。
  3. 10812086K – 老年代的當前使用量。
  4. (11901376K) – 老年代中可用內存總量。
  5. 10887844K – 當前堆內存的使用量。
  6. (12514816K) – 可用堆的總大小。
  7. 0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] – 此次暫停的持續時間, 以 user, system 和 real time 3個部分進行衡量。

階段 2: Concurrent Mark(併發標記). 在此階段, 垃圾收集器遍歷老年代, 標記所有的存活對象, 從前一階段 “Initial Mark” 找到的 root 根開始算起。 顧名思義, “併發標記”階段, 就是與應用程序同時運行,不用暫停的階段。 請注意, 並非所有老年代中存活的對象都在此階段被標記, 因爲在標記過程中對象的引用關係還在發生變化。

4. GC 算法(實現篇) - 圖7

在上面的示意圖中, “Current object” 旁邊的一個引用被標記線程併發刪除了。

2015-05-26T16:23:07.321-0200: 64.425: [CMS-concurrent-mark-start]

2015-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


  1. CMS-concurrent-mark – 併發標記(“Concurrent Mark”) 是CMS垃圾收集中的一個階段, 遍歷老年代並標記所有的存活對象。
  2. 035/0.035 secs – 此階段的持續時間, 分別是運行時間和相應的實際時間。
  3. [Times: user=0.07 sys=0.00, real=0.03 secs]Times 這部分對併發階段來說沒多少意義, 因爲是從併發標記開始時計算的,而這段時間內不僅併發標記在運行,程序也在運行

階段 3: Concurrent Preclean(併發預清理). 此階段同樣是與應用線程並行執行的, 不需要停止應用線程。 因爲前一階段是與程序併發進行的,可能有一些引用已經改變。如果在併發標記過程中發生了引用關係變化,JVM會(通過“Card”)將發生了改變的區域標記爲“髒”區(這就是所謂的卡片標記,Card Marking)。

4. GC 算法(實現篇) - 圖8

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

4. GC 算法(實現篇) - 圖9

此外, 本階段也會執行一些必要的細節處理, 併爲 Final Remark 階段做一些準備工作。

2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start]

2015-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]


  1. CMS-concurrent-preclean – 併發預清理階段, 統計此前的標記階段中發生了改變的對象。
  2. 0.016/0.016 secs – 此階段的持續時間, 分別是運行時間和對應的實際時間。
  3. [Times: user=0.02 sys=0.00, real=0.02 secs] – Times 這部分對併發階段來說沒多少意義, 因爲是從併發標記開始時計算的,而這段時間內不僅GC的併發標記在運行,程序也在運行。

階段 4: Concurrent Abortable Preclean(併發可取消的預清理). 此階段也不停止應用線程. 本階段嘗試在 STW 的 Final Remark 之前儘可能地多做一些工作。本階段的具體時間取決於多種因素, 因爲它循環做同樣的事情,直到滿足某個退出條件( 如迭代次數, 有用工作量, 消耗的系統時間,等等)。

2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-abortable-preclean-start]

2015-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


  1. CMS-concurrent-abortable-preclean – 此階段的名稱: “Concurrent Abortable Preclean”。
  2. 0.167/1.074 secs – 此階段的持續時間, 運行時間和對應的實際時間。有趣的是, 用戶時間明顯比時鐘時間要小很多。通常情況下我們看到的都是時鐘時間小於用戶時間, 這意味着因爲有一些並行工作, 所以運行時間纔會小於使用的CPU時間。這裏只進行了少量的工作 — 0.167秒的CPU時間,GC線程經歷了很多系統等待。從本質上講,GC線程試圖在必須執行 STW暫停之前等待儘可能長的時間。默認條件下,此階段可以持續最多5秒鐘。

  3. [Times: user=0.20 sys=0.00, real=1.07 secs] – “Times” 這部分對併發階段來說沒多少意義, 因爲是從併發標記開始時計算的,而這段時間內不僅GC的併發標記線程在運行,程序也在運行

此階段可能顯著影響STW停頓的持續時間, 並且有許多重要的配置選項和失敗模式。

階段 5: Final Remark(最終標記). 這是此次GC事件中第二次(也是最後一次)STW階段。本階段的目標是完成老年代中所有存活對象的標記. 因爲之前的 preclean 階段是併發的, 有可能無法跟上應用程序的變化速度。所以需要 STW暫停來處理複雜情況。

通常CMS會嘗試在年輕代儘可能空的情況運行 final remark 階段, 以免接連多次發生 STW 事件。

看起來稍微比之前的階段要複雜一些:

2015-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]


  1. 2015-05-26T16:23:08.447-0200: 65.550 – GC事件開始的時間. 包括時鐘時間,以及相對於JVM啓動的時間. 其中-0200表示西二時區,而中國所在的東8區爲 +0800
  2. CMS Final Remark – 此階段的名稱爲 “Final Remark”, 標記老年代中所有存活的對象,包括在此前的併發標記過程中創建/修改的引用。
  3. YG occupancy: 387920 K (613440 K) – 當前年輕代的使用量和總容量。
  4. [Rescan (parallel) , 0.0085125 secs] – 在程序暫停時重新進行掃描(Rescan),以完成存活對象的標記。此時 rescan 是並行執行的,消耗的時間爲 0.0085125秒
  5. weak refs processing, 0.0000243 secs]65.559 – 處理弱引用的第一個子階段(sub-phases)。 顯示的是持續時間和開始時間戳。
  6. class unloading, 0.0013120 secs]65.560 – 第二個子階段, 卸載不使用的類。 顯示的是持續時間和開始的時間戳。
  7. scrub string table, 0.0001759 secs – 最後一個子階段, 清理持有class級別 metadata 的符號表(symbol tables),以及內部化字符串對應的 string tables。當然也顯示了暫停的時鐘時間。
  8. 10812086K(11901376K) – 此階段完成後老年代的使用量和總容量
  9. 11200006K(12514816K) – 此階段完成後整個堆內存的使用量和總容量
  10. 0.0110730 secs – 此階段的持續時間。
  11. [Times: user=0.06 sys=0.00, real=0.01 secs] – GC事件的持續時間, 通過不同的類別來衡量: user, system and real time。

在5個標記階段完成之後, 老年代中所有的存活對象都被標記了, 現在GC將清除所有不使用的對象來回收老年代空間:

階段 6: Concurrent Sweep(併發清除). 此階段與應用程序併發執行,不需要STW停頓。目的是刪除未使用的對象,並收回他們佔用的空間。

4. GC 算法(實現篇) - 圖10

2015-05-26T16:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start] 2015-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]


  1. CMS-concurrent-sweep – 此階段的名稱, “Concurrent Sweep”, 清除未被標記、不再使用的對象以釋放內存空間。
  2. 0.027/0.027 secs – 此階段的持續時間, 分別是運行時間和實際時間
  3. [Times: user=0.03 sys=0.00, real=0.03 secs] – “Times”部分對併發階段來說沒有多少意義, 因爲是從併發標記開始時計算的,而這段時間內不僅是併發標記在運行,程序也在運行。

階段 7: Concurrent Reset(併發重置). 此階段與應用程序併發執行,重置CMS算法相關的內部數據, 爲下一次GC循環做準備。

2015-05-26T16:23:08.485-0200: 65.589: [CMS-concurrent-reset-start] 2015-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]


  1. CMS-concurrent-reset – 此階段的名稱, “Concurrent Reset”, 重置CMS算法的內部數據結構, 爲下一次GC循環做準備。
  2. 0.012/0.012 secs – 此階段的持續時間, 分別是運行時間和對應的實際時間
  3. [Times: user=0.01 sys=0.00, real=0.01 secs] – “Times”部分對併發階段來說沒多少意義, 因爲是從併發標記開始時計算的,而這段時間內不僅GC線程在運行,程序也在運行。

總之, CMS垃圾收集器在減少停頓時間上做了很多給力的工作, 大量的併發線程執行的工作並不需要暫停應用線程。 當然, CMS也有一些缺點,其中最大的問題就是老年代內存碎片問題, 在某些情況下GC會造成不可預測的暫停時間, 特別是堆內存較大的情況下。

G1 – Garbage First(垃圾優先算法)

G1最主要的設計目標是: 將STW停頓的時間和分佈變成可預期以及可配置的。事實上, G1是一款軟實時垃圾收集器, 也就是說可以爲其設置某項特定的性能指標. 可以指定: 在任意 xx 毫秒的時間範圍內, STW停頓不得超過 x 毫秒。 如: 任意1秒暫停時間不得超過5毫秒. Garbage-First GC 會盡力達成這個目標(有很大的概率會滿足, 但並不完全確定,具體是多少將是硬實時的[hard real-time])。

爲了達成這項指標, G1 有一些獨特的實現。首先, 堆不再分成連續的年輕代和老年代空間。而是劃分爲多個(通常是2048個)可以存放對象的 小堆區(smaller heap regions)。每個小堆區都可能是 Eden區, Survivor區或者Old區. 在邏輯上, 所有的Eden區和Survivor區合起來就是年輕代, 所有的Old區拼在一起那就是老年代:

4. GC 算法(實現篇) - 圖11

這樣的劃分使得 GC不必每次都去收集整個堆空間, 而是以增量的方式來處理: 每次只處理一部分小堆區,稱爲此次的回收集(collection set). 每次暫停都會收集所有年輕代的小堆區, 但可能只包含一部分老年代小堆區:

4. GC 算法(實現篇) - 圖12

G1的另一項創新, 是在併發階段估算每個小堆區存活對象的總數。用來構建回收集(collection set)的原則是: 垃圾最多的小堆區會被優先收集。這也是G1名稱的由來: garbage-first。

要啓用G1收集器, 使用的命令行參數爲:

java -XX:+UseG1GC com.mypackages.MyExecutableClass

Evacuation Pause: Fully Young(轉移暫停:純年輕代模式)

在應用程序剛啓動時, G1還未執行過(not-yet-executed)併發階段, 也就沒有獲得任何額外的信息, 處於初始的 fully-young 模式. 在年輕代空間用滿之後, 應用線程被暫停, 年輕代堆區中的存活對象被複制到存活區, 如果還沒有存活區,則選擇任意一部分空閒的小堆區用作存活區。

複製的過程稱爲轉移(Evacuation), 這和前面講過的年輕代收集器基本上是一樣的工作原理。轉移暫停的日誌信息很長,爲簡單起見, 我們去除了一些不重要的信息. 在併發階段之後我們會進行詳細的講解。此外, 由於日誌記錄很多, 所以並行階段和“其他”階段的日誌將拆分爲多個部分來進行講解:

0.134: [GC pause (G1 Evacuation Pause) (young), 0.0144119 secs]

[Parallel Time: 13.9 ms, GC Workers: 8]



[Code Root Fixup: 0.0 ms]

[Code Root Purge: 0.0 ms]

[Clear CT: 0.1 ms]

[Other: 0.4 ms]


[Eden: 24.0M(24.0M)->0.0B(13.0M) Survivors: 0.0B->3072.0K Heap: 24.0M(256.0M)->21.9M(256.0M)]
[Times: user=0.04 sys=0.04, real=0.02 secs]


  1. 0.134: [GC pause (G1 Evacuation Pause) (young), 0.0144119 secs] – G1轉移暫停,只清理年輕代空間。暫停在JVM啓動之後 134 ms 開始, 持續的系統時間爲 0.0144秒
  2. [Parallel Time: 13.9 ms, GC Workers: 8] – 表明後面的活動由8個 Worker 線程並行執行, 消耗時間爲13.9毫秒(real time)。
  3. – 爲閱讀方便, 省略了部分內容,請參考後文。
  4. [Code Root Fixup: 0.0 ms] – 釋放用於管理並行活動的內部數據。一般都接近於零。這是串行執行的過程。
  5. [Code Root Purge: 0.0 ms] – 清理其他部分數據, 也是非常快的, 但如非必要則幾乎等於零。這是串行執行的過程。
  6. [Other: 0.4 ms] – 其他活動消耗的時間, 其中有很多是並行執行的。
  7. – 請參考後文。
  8. [Eden: 24.0M(24.0M)->0.0B(13.0M) – 暫停之前和暫停之後, Eden 區的使用量/總容量。
  9. Survivors: 0.0B->3072.0K – 暫停之前和暫停之後, 存活區的使用量。
  10. Heap: 24.0M(256.0M)->21.9M(256.0M)] – 暫停之前和暫停之後, 整個堆內存的使用量與總容量。
  11. [Times: user=0.04 sys=0.04, real=0.02 secs] – GC事件的持續時間, 通過三個部分來衡量:
  • user – 在此次垃圾回收過程中, 由GC線程所消耗的總的CPU時間。
  • sys – GC過程中, 系統調用和系統等待事件所消耗的時間。
  • real – 應用程序暫停的時間。在並行GC(Parallel GC)中, 這個數字約等於: (user time + system time)/GC線程數。 這裏使用的是8個線程。 請注意,總是有一定比例的處理過程是不能並行化的。

說明: 系統時間(wall clock time, elapsed time), 是指一段程序從運行到終止,系統時鐘走過的時間。一般來說,系統時間都是要大於CPU時間

最繁重的GC任務由多個專用的 worker 線程來執行。下面的日誌描述了他們的行爲:

[Parallel Time: 13.9 ms, GC Workers: 8]

[GC Worker Start (ms): Min: 134.0, Avg: 134.1, Max: 134.1, Diff: 0.1]

[Ext Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum: 1.2]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]

[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]

[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]

[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.2, Diff: 0.2, Sum: 0.2]

[Object Copy (ms): Min: 10.8, Avg: 12.1, Max: 12.6, Diff: 1.9, Sum: 96.5]

[Termination (ms): Min: 0.8, Avg: 1.5, Max: 2.8, Diff: 1.9, Sum: 12.2]

[Termination Attempts: Min: 173, Avg: 293.2, Max: 362, Diff: 189, Sum: 2346]

[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]

GC Worker Total (ms): Min: 13.7, Avg: 13.8, Max: 13.8, Diff: 0.1, Sum: 110.2]

[GC Worker End (ms): Min: 147.8, Avg: 147.8, Max: 147.8, Diff: 0.0]


  1. [Parallel Time: 13.9 ms, GC Workers: 8] – 表明下列活動由8個線程並行執行,消耗的時間爲13.9毫秒(real time)。
  2. [GC Worker Start (ms) – GC的worker線程開始啓動時,相對於 pause 開始的時間戳。如果 MinMax 差別很大,則表明本機其他進程所使用的線程數量過多, 擠佔了GC的CPU時間。
  3. [Ext Root Scanning (ms) – 用了多長時間來掃描堆外(non-heap)的root, 如 classloaders, JNI引用, JVM的系統root等。後面顯示了運行時間, “Sum” 指的是CPU時間。
  4. [Code Root Scanning (ms) – 用了多長時間來掃描實際代碼中的 root: 例如局部變量等等(local vars)。
  5. [Object Copy (ms) – 用了多長時間來拷貝收集區內的存活對象。
  6. [Termination (ms) – GC的worker線程用了多長時間來確保自身可以安全地停止, 這段時間什麼也不用做, stop 之後該線程就終止運行了。
  7. [Termination Attempts – GC的worker 線程嘗試多少次 try 和 teminate。如果worker發現還有一些任務沒處理完,則這一次嘗試就是失敗的, 暫時還不能終止。
  8. [GC Worker Other (ms) – 一些瑣碎的小活動,在GC日誌中不值得單獨列出來。
  9. GC Worker Total (ms) – GC的worker 線程的工作時間總計。
  10. [GC Worker End (ms) – GC的worker 線程完成作業的時間戳。通常來說這部分數字應該大致相等, 否則就說明有太多的線程被掛起, 很可能是因爲壞鄰居效應(noisy neighbor) 所導致的。

此外,在轉移暫停期間,還有一些瑣碎執行的小活動。這裏我們只介紹其中的一部分, 其餘的會在後面進行討論。

[Other: 0.4 ms]

[Choose CSet: 0.0 ms]

[Ref Proc: 0.2 ms]

[Ref Enq: 0.0 ms]

[Redirty Cards: 0.1 ms]

[Humongous Register: 0.0 ms]

[Humongous Reclaim: 0.0 ms]

[Free CSet: 0.0 ms]


  1. [Other: 0.4 ms] – 其他活動消耗的時間, 其中有很多也是並行執行的。
  2. [Ref Proc: 0.2 ms] – 處理非強引用(non-strong)的時間: 進行清理或者決定是否需要清理。
  3. [Ref Enq: 0.0 ms] – 用來將剩下的 non-strong 引用排列到合適的 ReferenceQueue中。
  4. [Free CSet: 0.0 ms] – 將回收集中被釋放的小堆歸還所消耗的時間, 以便他們能用來分配新的對象。

Concurrent Marking(併發標記)

G1收集器的很多概念建立在CMS的基礎上,所以下面的內容需要你對CMS有一定的理解. 雖然也有很多地方不同, 但併發標記的目標基本上是一樣的. G1的併發標記通過 Snapshot-At-The-Beginning(開始時快照) 的方式, 在標記階段開始時記下所有的存活對象。即使在標記的同時又有一些變成了垃圾. 通過對象是存活信息, 可以構建出每個小堆區的存活狀態, 以便回收集能高效地進行選擇。

這些信息在接下來的階段會用來執行老年代區域的垃圾收集。在兩種情況下是完全地併發執行的: 一、如果在標記階段確定某個小堆區只包含垃圾; 二、在STW轉移暫停期間, 同時包含垃圾和存活對象的老年代小堆區。

當堆內存的總體使用比例達到一定數值時,就會觸發併發標記。默認值爲 45%, 但也可以通過JVM參數 InitiatingHeapOccupancyPercent 來設置。和CMS一樣, G1的併發標記也是由多個階段組成, 其中一些是完全併發的, 還有一些階段需要暫停應用線程。

階段 1: Initial Mark(初始標記)。 此階段標記所有從GC root 直接可達的對象。在CMS中需要一次STW暫停, 但G1裏面通常是在轉移暫停的同時處理這些事情, 所以它的開銷是很小的. 可以在 Evacuation Pause 日誌中的第一行看到(initial-mark)暫停:

1.631: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0062656 secs]

階段 2: Root Region Scan(Root區掃描). 此階段標記所有從 “根區域” 可達的存活對象。 根區域包括: 非空的區域, 以及在標記過程中不得不收集的區域。因爲在併發標記的過程中遷移對象會造成很多麻煩, 所以此階段必須在下一次轉移暫停之前完成。如果必須啓動轉移暫停, 則會先要求根區域掃描中止, 等它完成才能繼續掃描. 在當前版本的實現中, 根區域是存活的小堆區: y包括下一次轉移暫停中肯定會被清理的那部分年輕代小堆區。

1.362: [GC concurrent-root-region-scan-start]
1.364: [GC concurrent-root-region-scan-end, 0.0028513 secs]

階段 3: Concurrent Mark(併發標記). 此階段非常類似於CMS: 它只是遍歷對象圖, 並在一個特殊的位圖中標記能訪問到的對象. 爲了確保標記開始時的快照準確性, 所有應用線程併發對對象圖執行的引用更新,G1 要求放棄前面階段爲了標記目的而引用的過時引用。

這是通過使用 Pre-Write 屏障來實現的,(不要和之後介紹的 Post-Write 混淆, 也不要和多線程開發中的內存屏障(memory barriers)相混淆)。Pre-Write屏障的作用是: G1在進行併發標記時, 如果程序將對象的某個屬性做了變更, 就會在 log buffers 中存儲之前的引用。 由併發標記線程負責處理。

1.364: [GC concurrent-mark-start]
1.645: [GC co ncurrent-mark-end, 0.2803470 secs]

階段 4: Remark(再次標記). 和CMS類似,這也是一次STW停頓,以完成標記過程。對於G1,它短暫地停止應用線程, 停止併發更新日誌的寫入, 處理其中的少量信息, 並標記所有在併發標記開始時未被標記的存活對象。這一階段也執行某些額外的清理, 如引用處理(參見 Evacuation Pause log) 或者類卸載(class unloading)。

1.645: [GC remark 1.645: [Finalize Marking, 0.0009461 secs]
1.646: [GC ref-proc, 0.0000417 secs] 1.646: 
    [Unloading, 0.0011301 secs], 0.0074056 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]

階段 5: Cleanup(清理). 最後這個小階段爲即將到來的轉移階段做準備, 統計小堆區中所有存活的對象, 並將小堆區進行排序, 以提升GC的效率. 此階段也爲下一次標記執行所有必需的整理工作(house-keeping activities): 維護併發標記的內部狀態。

最後要提醒的是, 所有不包含存活對象的小堆區在此階段都被回收了。有一部分是併發的: 例如空堆區的回收,還有大部分的存活率計算, 此階段也需要一個短暫的STW暫停, 以不受應用線程的影響來完成作業. 這種STW停頓的日誌如下:

1.652: [GC cleanup 1213M->1213M(1885M), 0.0030492 secs]
[Times: user=0.01 sys=0.00, real=0.00 secs]

如果發現某些小堆區中只包含垃圾, 則日誌格式可能會有點不同, 如:

1.872: [GC cleanup 1357M->173M(1996M), 0.0015664 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]
1.874: [GC concurrent-cleanup-start]
1.876: [GC concurrent-cleanup-end, 0.0014846 secs]

Evacuation Pause: Mixed (轉移暫停: 混合模式)

能併發清理老年代中整個整個的小堆區是一種最優情形, 但有時候並不是這樣。併發標記完成之後, G1將執行一次混合收集(mixed collection), 不只清理年輕代, 還將一部分老年代區域也加入到 collection set 中。

混合模式的轉移暫停(Evacuation pause)不一定緊跟着併發標記階段。有很多規則和歷史數據會影響混合模式的啓動時機。比如, 假若在老年代中可以併發地騰出很多的小堆區,就沒有必要啓動混合模式。

因此, 在併發標記與混合轉移暫停之間, 很可能會存在多次 fully-young 轉移暫停。

添加到回收集的老年代小堆區的具體數字及其順序, 也是基於許多規則來判定的。 其中包括指定的軟實時性能指標, 存活性,以及在併發標記期間收集的GC效率等數據, 外加一些可配置的JVM選項. 混合收集的過程, 很大程度上和前面的 fully-young gc 是一樣的, 但這裏我們還要介紹一個概念: remembered sets(歷史記憶集)。

Remembered sets (歷史記憶集)是用來支持不同的小堆區進行獨立回收的。例如,在收集A、B、C區時, 我們必須要知道是否有從D區或者E區指向其中的引用, 以確定他們的存活性. 但是遍歷整個堆需要相當長的時間, 這就違背了增量收集的初衷, 因此必須採取某種優化手段. 其他GC算法有獨立的 Card Table 來支持年輕代的垃圾收集一樣, 而G1中使用的是 Remembered Sets。

如下圖所示, 每個小堆區都有一個 remembered set, 列出了從外部指向本區的所有引用。這些引用將被視爲附加的 GC root. 注意,在併發標記過程中,老年代中被確定爲垃圾的對象會被忽略, 即使有外部引用指向他們: 因爲在這種情況下引用者也是垃圾。

4. GC 算法(實現篇) - 圖13

接下來的行爲,和其他垃圾收集器一樣: 多個GC線程並行地找出哪些是存活對象,確定哪些是垃圾:

4. GC 算法(實現篇) - 圖14

最後, 存活對象被轉移到存活區(survivor regions), 在必要時會創建新的小堆區。現在,空的小堆區被釋放, 可用於存放新的對象了。

4. GC 算法(實現篇) - 圖15

爲了維護 remembered set, 在程序運行的過程中, 只要寫入某個字段,就會產生一個 Post-Write 屏障。如果生成的引用是跨區域的(cross-region),即從一個區指向另一個區, 就會在目標區的Remembered Set中,出現一個對應的條目。爲了減少 Write Barrier 造成的開銷, 將卡片放入Remembered Set 的過程是異步的, 而且經過了很多的優化. 總體上是這樣: Write Barrier 把髒卡信息存放到本地緩衝區(local buffer), 有專門的GC線程負責收集, 並將相關信息傳給被引用區的 remembered set。

混合模式下的日誌, 和純年輕代模式相比, 可以發現一些有趣的地方:

[[Update RS (ms): Min: 0.7, Avg: 0.8, Max: 0.9, Diff: 0.2, Sum: 6.1]

[Processed Buffers: Min: 0, Avg: 2.2, Max: 5, Diff: 5, Sum: 18]

[Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.8]

[Clear CT: 0.2 ms]

[Redirty Cards: 0.1 ms]



  1. [Update RS (ms) – 因爲 Remembered Sets 是併發處理的,必須確保在實際的垃圾收集之前, 緩衝區中的 card 得到處理。如果card數量很多, 則GC併發線程的負載可能就會很高。可能的原因是, 修改的字段過多, 或者CPU資源受限。

  2. [Processed Buffers – 每個 worker 線程處理了多少個本地緩衝區(local buffer)。

  3. [Scan RS (ms) – 用了多長時間掃描來自RSet的引用。
  4. [Clear CT: 0.2 ms] – 清理 card table 中 cards 的時間。清理工作只是簡單地刪除“髒”狀態, 此狀態用來標識一個字段是否被更新的, 供Remembered Sets使用。
  5. [Redirty Cards: 0.1 ms] – 將 card table 中適當的位置標記爲 dirty 所花費的時間。”適當的位置”是由GC本身執行的堆內存改變所決定的, 例如引用排隊等。

總結

通過本節內容的學習, 你應該對G1垃圾收集器有了一定了解。當然, 爲了簡潔, 我們省略了很多實現細節, 例如如何處理巨無霸對象(humongous objects)。 綜合來看, G1是HotSpot中最先進的準產品級(production-ready)垃圾收集器。重要的是, HotSpot 工程師的主要精力都放在不斷改進G1上面, 在新的java版本中,將會帶來新的功能和優化。

可以看到, G1 解決了 CMS 中的各種疑難問題, 包括暫停時間的可預測性, 並終結了堆內存的碎片化。對單業務延遲非常敏感的系統來說, 如果CPU資源不受限制,那麼G1可以說是 HotSpot 中最好的選擇, 特別是在最新版本的Java虛擬機中。當然,這種降低延遲的優化也不是沒有代價的: 由於額外的寫屏障(write barriers)和更積極的守護線程, G1的開銷會更大。所以, 如果系統屬於吞吐量優先型的, 又或者CPU持續佔用100%, 而又不在乎單次GC的暫停時間, 那麼CMS是更好的選擇。

總之: G1適合大內存,需要低延遲的場景

選擇正確的GC算法,唯一可行的方式就是去嘗試,並找出不對勁的地方, 在下一章我們將給出一般指導原則。

注意,G1可能會成爲Java 9的默認GC: http://openjdk.java.net/jeps/248

Shenandoah 的性能

譯註: Shenandoah: 謝南多厄河; 情人渡,水手謠; —> 此款GC暫時沒有標準的中文譯名; 翻譯爲大水手垃圾收集器?

我們列出了HotSpot中可用的所有 “準生產級” 算法。還有一種還在實驗室中的算法, 稱爲超低延遲垃圾收集器(Ultra-Low-Pause-Time Garbage Collector). 它的設計目標是管理大型的多核服務器上,超大型的堆內存: 管理 100GB 及以上的堆容量, GC暫停時間小於 10ms。 當然,也是需要和吞吐量進行權衡的: 沒有GC暫停的時候,算法的實現對吞吐量的性能損失不能超過10%

在新算法作爲準產品級進行發佈之前, 我們不準備去討論具體的實現細節, 但它也構建在前面所提到的很多算法的基礎上, 例如併發標記和增量收集。但其中有很多東西是不同的。它不再將堆內存劃分成多個代, 而是隻採用單個空間. 沒錯, Shenandoah 並不是一款分代垃圾收集器。這也就不再需要 card tables 和 remembered sets. 它還使用轉發指針(forwarding pointers), 以及Brooks 風格的讀屏障(Brooks style read barrier), 以允許對存活對象的併發複製, 從而減少GC暫停的次數和時間。

關於 Shenandoah 的更多信息,請參考博客: https://rkennke.wordpress.com/, JEP文檔: http://openjdk.java.net/jeps/189, 或者Google搜索 “Shenandoah GC“。

原文鏈接: GC Algorithms: Implementations

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章