麻豆小视频在线观看_中文黄色一级片_久久久成人精品_成片免费观看视频大全_午夜精品久久久久久久99热浪潮_成人一区二区三区四区

首頁 > 學院 > 開發設計 > 正文

4. GC 算法(實現篇) - GC參考手冊

2019-11-11 06:03:43
字體:
來源:轉載
供稿:網友

您應該已經閱讀了前面的章節:

垃圾收集簡介 - GC參考手冊java中的垃圾收集 - GC參考手冊GC 算法(基礎篇) - 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

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

151.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開始之前, 以及剛剛結束之后, 這兩個時間點內存使用情況的快照:

04_01_serial-gc-in-young-generation.png

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

>

2015-05-26T14:45:59.690-0200 – GC事件開始的時間. 其中-0200表示西二時區,而中國所在的東8區為 +0800。 172.829 – GC事件開始時,相對于JVM啟動時的間隔時間,單位是秒。

[DefNew: 629120K->629120K(629120K), 0.0000372 secs – 與上面的示例類似, 因為內存分配失敗,在年輕代中發生了一次 minor GC。此次GC同樣使用的是 DefNew 收集器, 讓年輕代的使用量從 629120K 降為 0。注意,JVM對此次GC的報告有些問題,誤將年輕代認為是完全填滿的。此次垃圾收集消耗了 0.0000372秒。

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

1203359K->755802K – 在垃圾收集之前和之后老年代的使用量。 (1398144K) – 老年代的總空間大小。 0.1855567 secs – 清理老年代所花的時間。 1832479K->755802K – 在垃圾收集之前和之后,整個堆內存的使用情況。 (2027264K) – 可用堆的總空間大小。 [Metaspace: 6741K->6741K(1056768K)] – 關于 Metaspace 空間, 同樣的信息??梢钥闯? 此次GC過程中 Metaspace 中沒有收集到任何垃圾。 [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事件開始之前, 以及剛剛結束之后的內存布局,可以用下面的示意圖來說明:

04_02_serial-gc-in-old-gen-java.png

Parallel GC(并行GC)

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

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

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

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

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

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

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

讓我們看看并行垃圾收集器的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

>

2015-05-26T14:27:40.915-0200 – GC事件開始的時間. 其中-0200表示西二時區,而中國所在的東8區為 +0800116.115 – GC事件開始時,相對于JVM啟動時的間隔時間,單位是秒。 GC – 用來區分 Minor GC 還是 Full GC 的標志。GC表明這是一次小型GC(Minor GC) Allocation Failure – 觸發垃圾收集的原因。本次GC事件, 是由于年輕代中沒有適當的空間存放新的數據結構引起的。 PSYoungGen – 垃圾收集器的名稱。這個名字表示的是在年輕代中使用的: 并行的 標記-復制(mark-copy), 全線暫停(STW) 垃圾收集器。 2694440K->1305132K – 在垃圾收集之前和之后的年輕代使用量。 (2796544K) – 年輕代的總大小。 9556775K->8438926K – 在垃圾收集之前和之后整個堆內存的使用量。 (11185152K) – 可用堆的總大小。 0.2406675 secs – GC事件持續的時間,以秒為單位。 [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 的對象從年輕代提升到老年代。

04_03_ParallelGC-in-Young-Generation-Java.png

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]

2015-05-26T14:27:41.155-0200 – GC事件開始的時間. 其中-0200表示西二時區,而中國所在的東8區為 +0800。 116.356 – GC事件開始時,相對于JVM啟動時的間隔時間,單位是秒。 我們可以看到, 此次事件在前一次 MinorGC完成之后立刻就開始了。 Full GC – 用來表示此次是 Full GC 的標志。Full GC表明本次清理的是年輕代和老年代。 Ergonomics – 觸發垃圾收集的原因。Ergonomics 表示JVM內部環境認為此時可以進行一次垃圾收集。 [PSYoungGen: 1305132K->0K(2796544K)] – 和上面的示例一樣, 清理年輕代的垃圾收集器是名為 “PSYoungGen” 的STW收集器, 采用標記-復制(mark-copy)算法。 年輕代使用量從 1305132K 變為 0, 一般 Full GC 的結果都是這樣。 ParOldGen – 用于清理老年代空間的垃圾收集器類型。在這里使用的是名為 ParOldGen 的垃圾收集器, 這是一款并行 STW垃圾收集器, 算法為 標記-清除-整理(mark-sweep-compact)。 7133794K->6597672K – 在垃圾收集之前和之后老年代內存的使用情況。 (8388608K) – 老年代的總空間大小。 8438926K->6597672K – 在垃圾收集之前和之后堆內存的使用情況。 (11185152K) – 可用堆內存的總容量。 [Metaspace: 6745K->6745K(1056768K)] – 類似的信息,關于 Metaspace 空間的。可以看出, 在GC事件中 Metaspace 里面沒有回收任何對象。 0.9158801 secs – GC事件持續的時間,以秒為單位。 [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事件前后的內存布局如下圖所示:

04_04_Java-ParallelGC-in-Old-Generation.png

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]

>

2015-05-26T16:23:07.219-0200 – GC事件開始的時間. 其中-0200表示西二時區,而中國所在的東8區為 +080064.322 – GC事件開始時,相對于JVM啟動時的間隔時間,單位是秒。 GC – 用來區分 Minor GC 還是 Full GC 的標志。GC表明這是一次小型GC(Minor GC) Allocation Failure – 觸發垃圾收集的原因。本次GC事件, 是由于年輕代中沒有適當的空間存放新的數據結構引起的。 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事件的持續時間, 通過三個部分來衡量: 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 的對象從年輕代提升到老年代。

04_05_ParallelGC-in-Young-Generation-Java.png

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 的直接引用, 以及由年輕代中存活對象所引用的對象。 后者也非常重要, 因為老年代是獨立進行回收的。

04_06_g1-06.png

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

>

2015-05-26T16:23:07.321-0200: 64.42 – GC事件開始的時間. 其中 -0200 是時區,而中國所在的東8區為 +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] – 此次暫停的持續時間, 以 user, system 和 real time 3個部分進行衡量。

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

04_07_g1-07.png

在上面的示意圖中, “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

>

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

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

04_08_g1-08.png

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

04_09_g1-09.png

此外, 本階段也會執行一些必要的細節處理, 并為 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]

>

CMS-concurrent-preclean – 并發預清理階段, 統計此前的標記階段中發生了改變的對象。 0.016/0.016 secs – 此階段的持續時間, 分別是運行時間和對應的實際時間。 [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

>

CMS-concurrent-abortable-preclean – 此階段的名稱: “Concurrent Abortable Preclean”。

0.167/1.074 secs – 此階段的持續時間, 運行時間和對應的實際時間。有趣的是, 用戶時間明顯比時鐘時間要小很多。通常情況下我們看到的都是時鐘時間小于用戶時間, 這意味著因為有一些并行工作, 所以運行時間才會小于使用的CPU時間。這里只進行了少量的工作 — 0.167秒的CPU時間,GC線程經歷了很多系統等待。從本質上講,GC線程試圖在必須執行 STW暫停之前等待盡可能長的時間。默認條件下,此階段可以持續最多5秒鐘。

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

>

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

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

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

04_10_g1-10.png

>

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]

>

CMS-concurrent-sweep – 此階段的名稱, “Concurrent Sweep”, 清除未被標記、不再使用的對象以釋放內存空間。 0.027/0.027 secs – 此階段的持續時間, 分別是運行時間和實際時間 [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]

>

CMS-concurrent-reset – 此階段的名稱, “Concurrent Reset”, 重置CMS算法的內部數據結構, 為下一次GC循環做準備。 0.012/0.012 secs – 此階段的持續時間, 分別是運行時間和對應的實際時間 [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區拼在一起那就是老年代:

04_11_g1-011.png

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

04_12_g1-02.png

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]

>

0.134: [GC pause (G1 Evacuation Pause) (young), 0.0144119 secs] – G1轉移暫停,只清理年輕代空間。暫停在JVM啟動之后 134 ms 開始, 持續的系統時間為 0.0144秒[Parallel Time: 13.9 ms, GC Workers: 8] – 表明后面的活動由8個 Worker 線程并行執行, 消耗時間為13.9毫秒(real time)。 – 為閱讀方便, 省略了部分內容,請參考后文。 [Code Root Fixup: 0.0 ms] – 釋放用于管理并行活動的內部數據。一般都接近于零。這是串行執行的過程。 [Code Root Purge: 0.0 ms] – 清理其他部分數據, 也是非??斓? 但如非必要則幾乎等于零。這是串行執行的過程。 [Other: 0.4 ms] – 其他活動消耗的時間, 其中有很多是并行執行的。 – 請參考后文。 [Eden: 24.0M(24.0M)->0.0B(13.0M) – 暫停之前和暫停之后, Eden 區的使用量/總容量。 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] – 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]

>

[Parallel Time: 13.9 ms, GC Workers: 8] – 表明下列活動由8個線程并行執行,消耗的時間為13.9毫秒(real time)。 [GC Worker Start (ms) – GC的worker線程開始啟動時,相對于 pause 開始的時間戳。如果 MinMax 差別很大,則表明本機其他進程所使用的線程數量過多, 擠占了GC的CPU時間。 [Ext Root Scanning (ms) – 用了多長時間來掃描堆外(non-heap)的root, 如 classloaders, JNI引用, JVM的系統root等。后面顯示了運行時間, “Sum” 指的是CPU時間。 [Code Root Scanning (ms) – 用了多長時間來掃描實際代碼中的 root: 例如局部變量等等(local vars)。 [Object Copy (ms) – 用了多長時間來拷貝收集區內的存活對象。 [Termination (ms) – GC的worker線程用了多長時間來確保自身可以安全地停止, 這段時間什么也不用做, stop 之后該線程就終止運行了。 [Termination Attempts – GC的worker 線程嘗試多少次 try 和 teminate。如果worker發現還有一些任務沒處理完,則這一次嘗試就是失敗的, 暫時還不能終止。 [GC Worker Other (ms) – 一些瑣碎的小活動,在GC日志中不值得單獨列出來。 GC Worker Total (ms) – GC的worker 線程的工作時間總計。 [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]

>

[Other: 0.4 ms] – 其他活動消耗的時間, 其中有很多也是并行執行的。 [Ref Proc: 0.2 ms] – 處理非強引用(non-strong)的時間: 進行清理或者決定是否需要清理。 [Ref Enq: 0.0 ms] – 用來將剩下的 non-strong 引用排列到合適的 ReferenceQueue中。 [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. 注意,在并發標記過程中,老年代中被確定為垃圾的對象會被忽略, 即使有外部引用指向他們: 因為在這種情況下引用者也是垃圾。

04_13_g1-03.png

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

04_14_g1-04.png

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

04_15_g1-05-v2.png

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

[Processed Buffers – 每個 worker 線程處理了多少個本地緩沖區(local buffer)。 [Scan RS (ms) – 用了多長時間掃描來自RSet的引用。 [Clear CT: 0.2 ms] – 清理 card table 中 cards 的時間。清理工作只是簡單地刪除“臟”狀態, 此狀態用來標識一個字段是否被更新的, 供Remembered Sets使用。 [Redirty Cards: 0.1 ms] – 將 card table 中適當的位置標記為 dirty 所花費的時間?!边m當的位置”是由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

翻譯人員: 鐵錨 http://blog.csdn.net/renfufei

翻譯時間: 2016年02月06日


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 午夜精品在线视频 | 中文字幕四区 | 国产亚洲精彩视频 | 国产无遮挡一级毛片 | 成年人视频免费看 | 精品一区二区久久久久久久网精 | 国产成人高清在线观看 | 亚洲国产视频在线 | 国产精品爱久久久久久久 | 宅男视频在线观看免费 | 天天夜天天操 | 欧美日韩大片在线观看 | 国产精品久久久久久久久久东京 | 欧美日韩免费看 | 国产四区 | 中国产一级毛片 | 久久久久电影网站 | 亚洲精品有限 | 国产1区2区在线观看 | 黄色三级三级三级 | 13一14毛片免费看 | 国产高潮好爽好大受不了了 | 青青国产在线视频 | 欧美中文在线 | 亚洲va久久久噜噜噜久牛牛影视 | 免费毛片在线视频 | 中文有码一区二区 | 国产一级毛片国语版 | 97伦理| 国产精品亚洲yourport | 免费看综艺策驰影院 | 黄色毛片观看 | 中文字幕在线观看日韩 | 色综合视频网 | 久久久久久久久久久综合 | 国产在线精品一区二区不卡 | 91中文在线 | 久久国产中文 | 久久久久久久久久综合 | 视频一区 日韩 | 日韩精品 |