GC參考手冊 —— GC 算法(實現篇)

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

咱們能夠選擇JVM內置的各類算法。若是不經過參數明確指定垃圾收集算法, 則會使用宿主平臺的默認實現。本章會詳細介紹各類算法的實現原理。java

下面是關於Java 8中各類組合的垃圾收集器概要列表,對於以前的Java版原本說,可用組合會有一些不一樣:git

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), 要麼是不支持或者是不太適用於生產環境。因此,接下來咱們只介紹下面這些組合及其工做原理:github

  • 年輕代和老年代的串行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

2015-05-26T14:45:37.987-0200 – GC事件開始的時間. 其中-0200表示西二時區,而中國所在的東8區爲 +0800151.126 – GC事件開始時,相對於JVM啓動時的間隔時間,單位是秒。
GC – 用來區分 Minor GC 仍是 Full GC 的標誌。GC代表這是一次小型GC(Minor GC)
Allocation Failure – 觸發 GC 的緣由。本次GC事件, 是因爲年輕代中沒有空間來存放新的數據結構引發的。
DefNew – 垃圾收集器的名稱。這個神祕的名字表示的是在年輕代中使用的: 單線程, 標記-複製(mark-copy), 全線暫停(STW) 垃圾收集器。
629119K->69888K – 在垃圾收集以前和以後年輕代的使用量。
(629120K) – 年輕代總的空間大小。
1619346K->1273247K – 在垃圾收集以前和以後整個堆內存的使用狀況。
(2027264K) – 可用堆的總空間大小。
0.0585007 secs – GC事件持續的時間,以秒爲單位。
[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開始以前, 以及剛剛結束以後, 這兩個時間點內存使用狀況的快照:

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)60.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事件開始以前, 以及剛剛結束以後的內存佈局,能夠用下面的示意圖來講明:

Parallel GC(並行GC)

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

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

能夠經過下面的任意一組命令行參數來指定並行GC:

java -XX:+UseParallelGC com.mypackages.MyExecutableClass java -XX:+UseParallelOldGC com.mypackages.MyExecutableClass java -XX:+UseParallelGC -XX:+UseParallelOldGC com.mypackages.MyExecutableClass

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

  • 在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-02001116.1152[ GC3 (Allocation Failure4)

[PSYoungGen52694440K->1305132K6 (2796544K)79556775K->8438926K8

(11185152K)90.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 的對象從年輕代提高到老年代。

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事件先後的內存佈局以下圖所示:

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 FinalRemark)[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-020064.322:[GC(Allocation Failure) 64.322: 

[ParNew613404K->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 的對象從年輕代提高到老年代。

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 FinalRemark)[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 的直接引用, 以及由年輕代中存活對象所引用的對象。 後者也很是重要, 由於老年代是獨立進行回收的。

2015-05-26T16:23:07.321-0200: 64.421: [GC (CMS Initial Mark1

[1 CMS-initial-mark: 10812086K1(11901376K)110887844K1(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 根開始算起。 顧名思義, 「併發標記」階段, 就是與應用程序同時運行,不用暫停的階段。 請注意, 並不是全部老年代中存活的對象都在此階段被標記, 由於在標記過程當中對象的引用關係還在發生變化。

在上面的示意圖中, 「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-mark1035/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)。

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

此外, 本階段也會執行一些必要的細節處理, 併爲 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-preclean0.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停頓。目的是刪除未使用的對象,並收回他們佔用的空間。

>

2015-05-26T16:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start] 2015-05-26T16:23:08.485-0200: 65.588: [CMS-concurrent-sweep0.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-reset0.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區拼在一塊兒那就是老年代:

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

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 開始的時間戳。若是 Min 和 Max 差異很大,則代表本機其餘進程所使用的線程數量過多, 擠佔了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(開始時快照) 的方式, 在標記階段開始時記下全部的存活對象。即便在標記的同時又有一些變成了垃圾. 經過對象是存活信息, 能夠構建出每一個小堆區的存活狀態, 以便回收集能高效地進行選擇。

這些信息在接下來的階段會用來執行老年代區域的垃圾收集。在兩種狀況下是徹底地併發執行的: 1、若是在標記階段肯定某個小堆區只包含垃圾; 2、在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. 注意,在併發標記過程當中,老年代中被肯定爲垃圾的對象會被忽略, 即便有外部引用指向他們: 由於在這種狀況下引用者也是垃圾。

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

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

爲了維護 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資源受限。

  1. [Processed Buffers – 每一個 worker 線程處理了多少個本地緩衝區(local buffer)。
  2. [Scan RS (ms) – 用了多長時間掃描來自RSet的引用。
  3. [Clear CT: 0.2 ms] – 清理 card table 中 cards 的時間。清理工做只是簡單地刪除「髒」狀態, 此狀態用來標識一個字段是否被更新的, 供Remembered Sets使用。
  4. [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暫停的次數和時間。

相關文章
相關標籤/搜索