Java垃圾回收手冊(cè)(四):垃圾回收算法實(shí)現(xiàn)

垃圾回收算法具體實(shí)現(xiàn)

翻譯原文 => plumbr Java GC handbook

前文參見:

Java垃圾回收手冊(cè)(一):初識(shí)垃圾回收
Java垃圾回收手冊(cè)(二):Java中的垃圾回收
Java垃圾回收手冊(cè)(三):垃圾回收算法基礎(chǔ)

在熟悉GC算法背后的核心概念之后,我們來看看JVM提供的各種GC算法的具體實(shí)現(xiàn)。對(duì)于大部分JVM來說,GC算法主要分為兩類,分別針對(duì)年輕代和老生代。

你可以選擇JVM提供的各種GC算法。如果不顯示指定的話,JVM會(huì)根據(jù)具體運(yùn)行平臺(tái)選擇默認(rèn)算法的。這篇文章我們來詳細(xì)探討一下這些算法的工作原理。

為了更直觀一些,這里先列出來各種GC算法的可能組合(這些組合只針對(duì)Java 8,其他版本可能稍有不同)。

年輕代 老生代 JVM參數(shù)
Incremental 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

如果覺得這個(gè)表格看起來比較復(fù)雜,先不用擔(dān)心。實(shí)際上常用的只有圖中用粗線標(biāo)出的4種組合,剩下要么已經(jīng)被棄用,要么不再被支持或者說在實(shí)際應(yīng)用環(huán)境中很少使用。所以我們接下來只討論這幾種組合的工作原理。

  • Serial GC(年輕代+老生代)
  • Parallel GC(年輕代+老生代)
  • Parallel New(年輕代) + CMS(老生代)
  • G1(本身不區(qū)分年輕代和老生代)

Serial GC

這類垃圾回收器針對(duì)年輕代使用標(biāo)記-拷貝算法,針對(duì)老生代使用標(biāo)記-清除-整理算法。顧名思義,這些收集器是單線程,無法并行執(zhí)行操作。他們也會(huì)產(chǎn)生讓所有應(yīng)用進(jìn)程都停止的stop-the-world停頓。

這種垃圾收集器無法利用現(xiàn)代非常普遍的多核CPU,無論CPU有幾個(gè)核,在JVM的垃圾回收過程中只能使用一個(gè)核。

使用以下參數(shù)可以針對(duì)年輕代和老生代開啟該垃圾收集器:

java -XX:+UseSerialGC com.mypackages.MyExecutableClass

該設(shè)置只有在CPU是單核,可用內(nèi)存只有幾百兆的JVM運(yùn)行環(huán)境情況下才推薦使用。對(duì)于大部分的服務(wù)端部署來講,很少使用這個(gè)組合。大部分服務(wù)端部署都是基于多核平臺(tái),選擇Serial GC就人為限制了資源的使用,這肯定會(huì)導(dǎo)致資源浪費(fèi),而這些資源本來可以用來降低延遲或者提高吞吐量。

我們現(xiàn)在來看看在使用Serial GC算法時(shí)的GC日志格式,看從中我們能獲得哪些信息。為此,我們需要打開JVM的GC日志功能。

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

此時(shí)的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內(nèi)部正在發(fā)生什么。事實(shí)上,這段日志表明有兩個(gè)GC事件發(fā)生了:一個(gè)是針對(duì)年輕代的GC,一個(gè)是針對(duì)整個(gè)堆的。我們逐個(gè)來具體分析一下日志的具體含義。

Minor GC

2015-05-26T14:45:37.987-02001:151.1262:[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開始時(shí)間
  2. 151.126:GC開始時(shí)間,相對(duì)JVM啟動(dòng)時(shí)間,單位是秒
  3. GC:區(qū)分是Minor GC還是Full GC,這里表示Minor GC
  4. Allocation Failure:產(chǎn)生GC的原因,這里是因?yàn)闊o法在年輕代為某個(gè)數(shù)據(jù)結(jié)構(gòu)分配空間導(dǎo)致觸發(fā)GC
  5. DefNew:收集器名字:一個(gè)單線程,使用標(biāo)記-拷貝算法,會(huì)產(chǎn)生的STW的垃圾收集器
  6. 629119K->69888K:GC前后年輕代使用情況
  7. (629120K):年輕代總?cè)萘?/li>
  8. 1619346K->1273247K:GC前后堆使用情況
  9. (2027264K):堆總?cè)萘?/li>
  10. 0.0585007 secs:GC事件持續(xù)時(shí)間,單位是秒
  11. [Times: user=0.06 sys=0.00, real=0.06 secs]:GC事件的時(shí)間開銷,分三個(gè)類別:
  • user:整個(gè)過程GC耗費(fèi)的全部CPU時(shí)間 => 垃圾收集器消耗的所有CPU執(zhí)行時(shí)間之和,所謂的CPU time
  • sys:系統(tǒng)耗費(fèi)時(shí)間,包括系統(tǒng)調(diào)用或者等待系統(tǒng)事件
  • real:應(yīng)用程序因GC被停止的時(shí)間。因?yàn)镾erial GC是單線程的,real time等于user time + sys time => 所謂的wall clock time,字面意思,墻上時(shí)間,該過程時(shí)鐘走過的時(shí)間

從上面的日志片段我們非常清楚的知道在GC發(fā)生過程中,JVM里面各個(gè)內(nèi)存空間的使用情況。GC之前,堆內(nèi)存總共使用了1619346K,這其中年輕代內(nèi)存使用了629119K,從而可以推算出老生代的內(nèi)存使用量為990,227K。

我們通過簡單的計(jì)算還可以從這里面了解到更重要的信息:GC之后,年輕代內(nèi)存使用量減少559,231K,但是整個(gè)堆的內(nèi)存使用量只減少346,099K,從而我們可以推算出,在該GC過程中,有213,132K大小的對(duì)象從年輕代晉升到老生代。

用圖來展示GC前和GC后內(nèi)存使用變化如下:

serial-gc-in-young-generation.png

Full 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開始時(shí)間
  2. 172.829:GC開始時(shí)間,相對(duì)JVM啟動(dòng)時(shí)間,單位是秒
  3. [DefNew: 629120K->629120K(629120K), 0.0000372 secs:和上面的例子類似,因?yàn)閮?nèi)存分配失敗導(dǎo)致了一次年輕代的GC,同樣是一個(gè)叫做DefNew的收集器被運(yùn)行了,GC后,年輕代內(nèi)存使用從629120K降到0。這里日志里顯示GC后依舊是629120K是JVM的bug
  4. Tenured:老生代垃圾收集器名字:一個(gè)單線程,使用標(biāo)記-清除-整理算法,會(huì)產(chǎn)生STW的垃圾回收器
  5. 1203359K->755802K:GC前后老生代使用情況
  6. (1398144K):老生代總?cè)萘?/li>
  7. 0.1855567 secs:老生代GC所耗時(shí)間
  8. 1832479K->755802K:GC(年輕代+老生代)前后堆使用情況
  9. (2027264K):堆總?cè)萘?/li>
  10. [Metaspace: 6741K->6741K(1056768K)]:關(guān)于元空間的類似信息
  11. [Times: user=0.18 sys=0.00, real=0.18 secs]:GC事件的時(shí)間開銷,分三個(gè)類別:
  • user:整個(gè)過程GC耗費(fèi)的全部CPU時(shí)間
  • sys:系統(tǒng)耗費(fèi)時(shí)間,包括系統(tǒng)調(diào)用或者等待系統(tǒng)事件
  • real:應(yīng)用程序因GC被停止的時(shí)間。因?yàn)镾erial GC是單線程的,real time等于user time + sys time

這個(gè)跟Minor GC的區(qū)別非常明顯:除了年輕代,在這次GC過程中,老生代和元空間也被清除了。

用圖來展示GC前和GC后內(nèi)存使用變化如下:

serial-gc-in-old-gen-java.png

Parallel GC

這個(gè)組合包括在年輕代使用標(biāo)記-復(fù)制算法的GC,在老生代使用標(biāo)記-清除-整理算法的GC。年輕代和老生代的GC都會(huì)導(dǎo)致STW事件產(chǎn)生,所有應(yīng)用在GC過程中都必須停下來。但在標(biāo)記和復(fù)制/整理階段是使用多線程,這也就是為什么稱之為“并行”GC。使用這個(gè)算法,可以大大減少GC時(shí)間。

該垃圾收集器使用的線程數(shù)可以通過參數(shù) -XX:ParallelGCThreads=NNN 來設(shè)置,默認(rèn)為系統(tǒng)CPU核數(shù)。

使用以下參數(shù)之一可以開啟該GC算法:

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

Parallel GC適合多核機(jī)器,并且你的主要目標(biāo)是提高吞吐量。更高的吞吐量得益于更高效的使用系統(tǒng)資源。

  • GC期間,所有核并行清除垃圾,從而可以獲得更短的停頓時(shí)間
  • 在GC周期之間,不消耗任何系統(tǒng)資源

另外,因?yàn)樗械氖占A段仍然是不允許被打斷的,這種垃圾收集器仍有可能導(dǎo)致長時(shí)間停頓的發(fā)生。如果你的主要目標(biāo)是延遲的話,你應(yīng)該考慮下一節(jié)介紹的CMS。

我們現(xiàn)在來看看在使用Parallel GC算法時(shí)的GC日志格式,看從中我們能獲得哪些信息。同樣我們截取兩個(gè)日志片段,一個(gè)是Minor GC的,一個(gè)是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

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開始時(shí)間
  2. 116.115:GC開始時(shí)間,相對(duì)JVM啟動(dòng)時(shí)間,單位是秒
  3. GC:區(qū)分是Minor GC還是Full GC,這里表示Minor
  4. Allocation Failure:產(chǎn)生GC的原因,這里是因?yàn)闊o法在年輕代為某個(gè)數(shù)據(jù)結(jié)構(gòu)分配空間導(dǎo)致觸發(fā)GC
  5. PSYoungGen:年輕代垃圾收集器名字:一個(gè)并行的,使用標(biāo)記-拷貝算法,會(huì)產(chǎn)生STW的垃圾回收器
  6. 2694440K->1305132K:GC前后年輕代使用情況
  7. (2796544K):年輕代總?cè)萘?/li>
  8. 9556775K->8438926K:GC前后堆使用情況
  9. (11185152K):堆總?cè)萘?/li>
  10. 0.2406675 secs:GC事件持續(xù)時(shí)間,單位是秒
  11. [Times: user=1.77 sys=0.01, real=0.24 secs]:GC事件的時(shí)間開銷,分三個(gè)類別:
  • user:整個(gè)過程GC耗費(fèi)的全部CPU時(shí)間
  • sys:系統(tǒng)耗費(fèi)時(shí)間,包括系統(tǒng)調(diào)用或者等待系統(tǒng)事件
  • real:應(yīng)用程序因GC被停止的時(shí)間。對(duì)于Parallel GC來說,real time接近于(user time + sys time)/GC線程數(shù),這里使用了8個(gè)線程。因?yàn)槟承┗顒?dòng)不能并行,所以這個(gè)值會(huì)稍稍大一點(diǎn)。

簡單來說,GC之前,堆使用量是9,556,775K,其中年輕代使用量是2,694,440K,可知老生代使用量為6,862,335K。GC之后,年輕代的使用量減少了1,389,308K,而整個(gè)堆的使用量只減少1,117,849K,可知該過程中有271,459K對(duì)象從年輕代晉升到老生代。如圖:

ParallelGC-in-Young-Generation-Java.png

Full GC

2015-05-26T14:27:41.155-02001:116.3562:[Full GC3 (Ergonomics4)[PSYoungGen: 1305132K->0K(2796544K)]5[ParOldGen6:7133794K->6597672K7(8388608K)8] 8438926K->6597672K9(11185152K)10, [Metaspace: 6745K->6745K(1056768K)] 11, 0.9158801 secs12, [Times: user=4.49 sys=0.64, real=0.92 secs]13

  1. 2015-05-26T14:27:41.155-0200:GC開始時(shí)間
  2. 116.356:GC開始時(shí)間,相對(duì)JVM啟動(dòng)時(shí)間,單位是秒
  3. Full GC:表示這是Full GC,垃圾清理包括年輕代和老生代
  4. Ergonomics:產(chǎn)生GC的原因,這里表示JVM的內(nèi)部工效邏輯判斷目前是進(jìn)行垃圾回收的好時(shí)機(jī)
  5. [PSYoungGen: 1305132K->0K(2796544K)]:跟上面的類似,在年輕代使用了一個(gè)并行的,使用標(biāo)記-拷貝算法,會(huì)產(chǎn)生STW的PSYoungGen垃圾回收器,回收之后年輕代被清空,這也是一次Full GC的典型結(jié)果
  6. ParOldGen:老生代使用的垃圾收集器名字:一個(gè)并行的,使用標(biāo)記-清除-整理算法,會(huì)產(chǎn)生STW的垃圾回收器
  7. 7133794K->6597672K:GC前后老生代使用情況
  8. (8388608K):老生代總?cè)萘?/li>
  9. 8438926K->6597672K:GC前后堆使用情況
  10. (11185152K):堆總?cè)萘?/li>
  11. [Metaspace: 6745K->6745K(1056768K)]:關(guān)于元空間的類似信息
  12. 0.9158801 secs:GC事件持續(xù)時(shí)間,單位是秒
  13. [Times: user=4.49 sys=0.64, real=0.92 secs]:GC事件的時(shí)間開銷,分三個(gè)類別:
  • user:整個(gè)過程GC耗費(fèi)的全部CPU時(shí)間
  • sys:系統(tǒng)耗費(fèi)時(shí)間,包括系統(tǒng)調(diào)用或者等待系統(tǒng)事件
  • real:應(yīng)用程序因GC被停止的時(shí)間。對(duì)于Parallel GC來說,real time接近于(user time + sys time)/GC線程數(shù),這里使用了8個(gè)線程。因?yàn)槟承┗顒?dòng)不能并行,所以這個(gè)值會(huì)稍稍大一點(diǎn)。

同樣,這個(gè)跟Minor GC的區(qū)別非常明顯:除了年輕代,在這次GC過程中,老生代和元空間也被清除了。

用圖來展示GC前和GC后內(nèi)存使用變化如下:

Java-ParallelGC-in-Old-Generation.png

CMS

該垃圾收集器組合的官方名字是“Mostly Concurrent Mark and Sweep Garbage Collector”。它在年輕代使用并行的,使用標(biāo)記-拷貝算法,會(huì)產(chǎn)生STW的GC,在老生代使用并發(fā)的,使用標(biāo)記-清除算法的GC。

這個(gè)垃圾收集器設(shè)計(jì)初衷是在老生代垃圾收集中避免長時(shí)間停頓。它通過兩個(gè)手段來實(shí)現(xiàn)該目標(biāo)。一:不對(duì)老生代內(nèi)存進(jìn)行整理操作,而是使用空閑列表來管理那些可回收再利用的內(nèi)存空間。二:在標(biāo)記-清除階段,和應(yīng)用程序并發(fā)的做掉絕大分工作。這意味著垃圾回收并不會(huì)顯示停止應(yīng)用程序線程來執(zhí)行這些操作。盡管如此,垃圾回收器線程還是會(huì)跟應(yīng)用程序線程搶占CPU時(shí)間。默認(rèn)情況下,這種GC算法使用的線程數(shù)量等于機(jī)器CPU核數(shù)的1/4。

可用通過以下參數(shù)啟用該垃圾回收器:

java -XX:+UseConcMarkSweepGC com.mypackages.MyExecutableClass

如果應(yīng)用程序運(yùn)行在多核機(jī)器上,且你的主要目標(biāo)是降低延遲,CMS會(huì)是一個(gè)不錯(cuò)的選擇。降低單次GC停頓時(shí)間直接影響終端用戶對(duì)應(yīng)用程序的感受,會(huì)讓用戶感覺應(yīng)用程序響應(yīng)更快。由于在大部分時(shí)間里,總有一些CPU資源被GC使用而不是用來執(zhí)行你的應(yīng)用程序代碼,所以對(duì)于計(jì)算密集型應(yīng)用來說,CMS在吞吐量上不如Parallel GC。

和之前的GC算法一樣,讓我們?cè)俅瓮ㄟ^查看包含一次Minor GC和一次Major GC的GC日志來看看該算法是如何在實(shí)際中應(yīng)用的。

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

2015-05-26T16:23:07.219-02001: 64.3222:[GC3(Allocation Failure4) 64.322: [ParNew5: 613404K->68068K6(613440K) 7, 0.1020465 secs8] 10885349K->10880154K 9(12514816K)10, 0.1021309 secs11][Times: user=0.78 sys=0.01, real=0.11 secs]12

  1. 2015-05-26T16:23:07.219-0200:GC開始時(shí)間
  2. 64.322:GC開始時(shí)間,相對(duì)JVM啟動(dòng)時(shí)間,單位是秒
  3. GC:區(qū)分是Minor GC還是Full GC,這里表示Minor
  4. Allocation Failure:產(chǎn)生GC的原因,這里是因?yàn)闊o法在年輕代為某個(gè)數(shù)據(jù)結(jié)構(gòu)分配空間導(dǎo)致觸發(fā)GC
  5. ParNew:年輕代垃圾收集器名字:一個(gè)并行的,使用標(biāo)記-拷貝算法,會(huì)產(chǎn)生STW的垃圾回收器,該垃圾收集器是用來跟老生代的CMS配合使用的
  6. 613404K->68068K:GC前后年輕代使用情況
  7. (613440K):年輕代總?cè)萘?/li>
  8. 0.1020465 secs:GC事件持續(xù)時(shí)間
  9. 10885349K->10880154K:GC前后堆使用情況
  10. (12514816K):堆總?cè)萘?/li>
  11. 0.1021309 secs:GC標(biāo)記和拷貝年輕代里面活對(duì)象耗費(fèi)的時(shí)間,這里面包括和老生代CMS通訊開銷,對(duì)象晉升到老年代的開銷,GC結(jié)束前清理工作的開銷
  12. [Times: user=0.78 sys=0.01, real=0.11 secs]:GC事件的時(shí)間開銷,分三個(gè)類別:
  • user:整個(gè)過程GC耗費(fèi)的全部CPU時(shí)間
  • sys:系統(tǒng)耗費(fèi)時(shí)間,包括系統(tǒng)調(diào)用或者等待系統(tǒng)事件
  • real:應(yīng)用程序因GC被停止的時(shí)間。對(duì)于Parallel GC來說,real time接近于(user time + sys time)/GC線程數(shù),這里使用了8個(gè)線程。因?yàn)槟承┗顒?dòng)不能并行,所以這個(gè)值會(huì)稍稍大一點(diǎn)。

從上面可以看出,GC之前,堆使用量是10,885,349K,其中年輕代使用量是613,404K,可知老生代使用量為10,271,945K。GC之后,年輕代的使用量減少了545,336K,而整個(gè)堆的使用量只減少5,195K,可知該過程中有540,141K對(duì)象從年輕代晉升到老生代。如圖:

ParallelGC-in-Young-Generation-Java.png

Full GC

當(dāng)你已經(jīng)開始熟悉垃圾收集器的日志格式的時(shí)候,這一節(jié)將介紹一個(gè)格式完全不同的日志格式。下面的這個(gè)日志輸出包含了CMS日志的所有階段,為了更好的解釋清楚,我們按照階段來逐個(gè)分析各個(gè)階段的日志含義。

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]

請(qǐng)記住一點(diǎn):在真實(shí)世界里,在對(duì)老生代進(jìn)行并發(fā)垃圾回收的同時(shí),年輕代的GC隨時(shí)可能發(fā)生。這個(gè)時(shí)候,Minor GC和Full GC的日志就會(huì)穿插的出現(xiàn)在GC日志文件里面。

Phase 1: Initial Mark。 這是CMS GC過程中兩次STW中的一次。這個(gè)階段的目標(biāo)是標(biāo)記出老生代里面符合條件的對(duì)象,這些對(duì)象或者是從GC roots直接指向的,或者被年輕代活著對(duì)象指向的。后者非常重要,因?yàn)槔仙菃为?dú)回收的。

cms-initial-mark.png

2015-05-26T16:23:07.321-0200: 64.421: [GC (CMS Initial Mark2[1 CMS-initial-mark: 10812086K3(11901376K)4] 10887844K5(12514816K)6, 0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]7

  1. 2015-05-26T16:23:07.321-0200: 64.42:GC開始時(shí)間
  2. CMS Initial Mark:該階段的名字
  3. 10812086K:當(dāng)前老生代使用量
  4. (11901376K):老生代總?cè)萘?/li>
  5. 10887844K:當(dāng)前堆使用量
  6. (12514816K):堆總?cè)萘?/li>
  7. [Times: user=0.00 sys=0.00, real=0.00 secs]:該階段的耗時(shí)

Phase 2: Concurrent Mark。 在這個(gè)階段,垃圾收集器從上一個(gè)階段找到的所有根節(jié)點(diǎn)開始遍歷整個(gè)老生代,標(biāo)記所有活著的對(duì)象。這個(gè)階段是和應(yīng)用程序并發(fā)執(zhí)行的,不會(huì)停止應(yīng)用程序進(jìn)程。這里需要注意的是,因?yàn)槭遣l(fā)執(zhí)行,程序可能在標(biāo)記過程中修改引用,所以并不是所有的活著都會(huì)被標(biāo)記出。

cms-concurrent-mark.png

如圖所示,在標(biāo)記的過程中,“Current obj”指向另一個(gè)對(duì)象的引用被刪除了。

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 secs2] [Times: user=0.07 sys=0.00, real=0.03 secs]3

  1. CMS-concurrent-mark:該階段的名字
  2. 035/0.035 secs:該階段消耗的時(shí)間,分別展示了clock時(shí)間和cpu時(shí)間
  3. [Times: user=0.07 sys=0.00, real=0.03 secs]:并發(fā)階段的時(shí)間字段參考意義不大

Phase 3: Concurrent Preclean。 這又是一個(gè)并發(fā)階段,和應(yīng)用程序并發(fā)執(zhí)行的,不會(huì)停止應(yīng)用程序進(jìn)程。在前一個(gè)階段和應(yīng)用程序并發(fā)執(zhí)行的過程中,一些引用可能被修改,當(dāng)修改發(fā)生時(shí),JVM都會(huì)把包含該修改對(duì)象的堆區(qū)域(又被稱為卡片)標(biāo)記為“臟數(shù)據(jù)”(又被稱為卡片標(biāo)記)

cms-concurrent-preclean.png

在這個(gè)階段,從臟數(shù)據(jù)到達(dá)的對(duì)象也會(huì)被標(biāo)記成活對(duì)象,標(biāo)記完成之后,卡片臟數(shù)據(jù)標(biāo)記會(huì)被清除。

cms-concurrent-preclean-2.png

該階段還會(huì)為Final Remark階段做一些必要的整理記錄和準(zhǔn)備的工作。

2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start]
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-preclean1: 0.016/0.016 secs2] [Times: user=0.02 sys=0.00, real=0.02 secs]3

  1. CMS-concurrent-preclean:該階段的名字
  2. 0.016/0.016 secs:該階段消耗的時(shí)間,分別展示了clock時(shí)間和cpu時(shí)間
  3. [Times: user=0.02 sys=0.00, real=0.02 secs]:并發(fā)階段的時(shí)間字段參考意義不大

Phase 4: Concurrent Abortable Preclean。 還是一個(gè)并發(fā)階段,不會(huì)停止應(yīng)用程序進(jìn)程。這個(gè)階段嘗試著去承擔(dān)STW的Final Remark階段足夠多的工作。這個(gè)階段持續(xù)的時(shí)間依賴很多的因素,由于這個(gè)階段是重復(fù)的做相同的事情直到某些中止條件被滿足為止(中止條件包括:重復(fù)的次數(shù)、多少量的工作、累計(jì)執(zhí)行時(shí)間等等)。

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:該階段的名字
  2. 0.167/1.074 secs:該階段消耗的時(shí)間,分別展示了clock時(shí)間和cpu時(shí)間。這里會(huì)發(fā)現(xiàn)user time要比clock time小得多。一般我們都是看到real time要比user time小,這是因?yàn)橛行┕ぷ鞅徊l(fā)執(zhí)行了,所以elapsed clock time要比使用的CPU time少。這里我們僅有少量的工作,0.167秒的CPU時(shí)間,垃圾回收器線程花費(fèi)了很多時(shí)間在等待上。也就是說,他們?cè)诒M量推遲STW的到來,默認(rèn)情況下,這個(gè)階段可能持續(xù)5秒
  3. [Times: user=0.20 sys=0.00, real=1.07 secs]:并發(fā)階段的時(shí)間字段參考意義不大

這個(gè)階段可能對(duì)即將到來的STW階段影響非常大,有很多重要的配置參數(shù)和失敗方式。

Phase 5: Final Remark。 這是第二個(gè)也是最后一個(gè)STW階段。這個(gè)階段的目標(biāo)就是最終標(biāo)記老生代的所有活著的對(duì)象。因?yàn)橹暗膒reclean是并發(fā)階段,他們可能無法趕上應(yīng)用程序的修改速度。所以需要一個(gè)STW暫停來完成整個(gè)標(biāo)記過程。

通常CMS試著在年輕代盡可能空的情況下執(zhí)行最終標(biāo)記,試圖減少多個(gè)STW階段一個(gè)接著一個(gè)的產(chǎn)生的可能性。

這個(gè)階段的GC日志比前面階段要復(fù)雜一些。

2015-05-26T16:23:08.447-0200: 65.5501: [GC (CMS Final Remark2) [YG occupancy: 387920 K (613440 K)3]65.550: [Rescan (parallel) , 0.0085125 secs]465.559: [weak refs processing, 0.0000243 secs]65.5595: [class unloading, 0.0013120 secs]65.5606: [scrub string table, 0.0001759 secs7][1 CMS-remark: 10812086K(11901376K)8] 11200006K(12514816K) 9, 0.0110730 secs10] [Times: user=0.06 sys=0.00, real=0.01 secs]11

  1. 2015-05-26T16:23:08.447-0200: 65.550:GC開始時(shí)間
  2. CMS Final Remark:該階段的名字
  3. YG occupancy: 387920 K (613440 K):當(dāng)前年輕代的使用量和總?cè)萘?/li>
  4. [Rescan (parallel) , 0.0085125 secs]:應(yīng)用程序停止過程中標(biāo)記所有活著對(duì)象所耗的時(shí)間
  5. [weak refs processing, 0.0000243 secs]65.559:第一個(gè)子階段:處理弱引用所耗時(shí)間
  6. [class unloading, 0.0013120 secs]65.560:第二個(gè)子階段:卸載不用類所耗時(shí)間
  7. [scrub string table, 0.0001759 secs:最后一個(gè)子階段:清除字符表(存儲(chǔ)類級(jí)別元數(shù)據(jù))和字符串表(存儲(chǔ)駐留字符串)所耗時(shí)間,停頓的clock time也被包括在里面
  8. 10812086K(11901376K):標(biāo)記完之后老生代使用量和總?cè)萘?/li>
  9. 11200006K(12514816K):標(biāo)記完之后堆使用量和總?cè)萘?/li>
  10. 0.0110730 secs:該階段所耗時(shí)間
  11. [Times: user=0.06 sys=0.00, real=0.01 secs]:GC事件的時(shí)間開銷,分三個(gè)類別

在經(jīng)歷五個(gè)標(biāo)記階段之后,老生代所有活著的對(duì)象都被標(biāo)記完畢,現(xiàn)在垃圾收集器就準(zhǔn)備通過清除老生代來回收所有不使用對(duì)象的存儲(chǔ)空間。

Phase 6: Concurrent Sweep。 該階段也是跟應(yīng)用程序并發(fā)執(zhí)行,不需要STW停頓。這個(gè)階段的目標(biāo)是清除那些不再使用的對(duì)象,并回收他們占用的內(nèi)存空間以備后續(xù)使用。

cms-concurrent-sweep.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-sweep1: 0.027/0.027 secs2] [Times: user=0.03 sys=0.00, real=0.03 secs] 3

  1. CMS-concurrent-sweep:該階段名字
  2. 0.027/0.027 secs:該階段消耗的時(shí)間,分別展示了clock時(shí)間和cpu時(shí)間。
  3. [Times: user=0.03 sys=0.00, real=0.03 secs]:并發(fā)階段的時(shí)間字段參考意義不大

Phase 7: Concurrent Reset。 并發(fā)執(zhí)行階段,重置CMS算法的內(nèi)部數(shù)據(jù)結(jié)構(gòu),為下一次執(zhí)行做準(zhǔn)備。

2015-05-26T16:23:08.485-0200: 65.589: [CMS-concurrent-reset-start] 2015-05-26T16:23:08.497-0200: 65.601: [CMS-concurrent-reset1: 0.012/0.012 secs2] [Times: user=0.01 sys=0.00, real=0.01 secs]3

  1. CMS-concurrent-reset:該階段名字
  2. 0.012/0.012 secs:該階段消耗的時(shí)間,分別展示了clock時(shí)間和cpu時(shí)間。
  3. [Times: user=0.01 sys=0.00, real=0.01 secs]:并發(fā)階段的時(shí)間字段參考意義不大

總的來講,CMS垃圾回收器通過把大量的工作放到不需要應(yīng)用程序停頓的并發(fā)線程里面去做掉,大大減少了應(yīng)用程序的停頓時(shí)間。但是,它也有自己的缺點(diǎn),最明顯的就是老生代內(nèi)存碎片問題和某些情況下停頓時(shí)間的不可預(yù)測性,特別是在大內(nèi)存堆的情況下

G1

G1的一個(gè)重要的設(shè)計(jì)目標(biāo)就是由GC導(dǎo)致的STW停頓的持續(xù)時(shí)間可預(yù)測,持續(xù)時(shí)間長短可配置。實(shí)際上,G1是一種軟實(shí)時(shí)垃圾收集器,這意味著你可以給它設(shè)定具體的性能指標(biāo)。你可以要求在任意給定Y毫秒范圍內(nèi)STW停頓持續(xù)時(shí)間不超過X毫秒,比如:在任意一秒鐘內(nèi)不超過5毫秒。G1垃圾收集器會(huì)盡自己最大努力盡可能滿足這個(gè)目標(biāo)設(shè)定(這個(gè)目標(biāo)不一定能達(dá)成,否則就是硬實(shí)時(shí)了)。

為了達(dá)成這個(gè)設(shè)計(jì)目標(biāo),G1提供了幾個(gè)新思路。首先,堆不是被劃分成連續(xù)的年輕代和老生代,而是被劃分成一些(典型是2048個(gè))更小的堆區(qū)域塊,這些區(qū)域塊用來存儲(chǔ)對(duì)象。任意一個(gè)區(qū)域塊可能是一個(gè)伊甸區(qū),也可能是一個(gè)存活區(qū),還可能是一個(gè)老年區(qū)。邏輯上把所有的伊甸區(qū)和存活區(qū)合起來稱作年輕代,所有的老年區(qū)合起來稱作老生代。

g1-01.png

這樣的話,GC可以采用增量的方式,每次回收一些區(qū)域塊,而不是對(duì)整個(gè)堆進(jìn)行垃圾回收。在每次停頓時(shí),會(huì)對(duì)所有年輕代區(qū)域進(jìn)行垃圾回收,某些老生代區(qū)域可能也被包含進(jìn)來一起。

g1-02.png

其次,G1在并發(fā)階段它會(huì)計(jì)算每個(gè)區(qū)域塊包含多少活對(duì)象。這個(gè)用來構(gòu)建單次收集集合:包含最多垃圾的區(qū)域塊最先進(jìn)行垃圾收集。這也是它的名字由來:garbage-first。

啟用G1垃圾收集器:

java -XX:+UseG1GC com.mypackages.MyExecutableClass

Evacuation Pause:全年輕代模式

在應(yīng)用程序生命周期初期,G1還未執(zhí)行并發(fā)標(biāo)記階段,對(duì)堆內(nèi)存無任何額外信息。所以它最開始是運(yùn)行于全年輕代模式。當(dāng)整個(gè)年輕代被對(duì)象裝滿,應(yīng)用程序線程被停止了,年輕代的活對(duì)象被拷貝到存活區(qū),或者被拷貝到任何空閑區(qū)域塊,這些空閑區(qū)域塊也就變成了存活區(qū)塊。

這個(gè)拷貝過程被稱作Evacuation,它的工作方式跟之前介紹的年輕代垃圾收集器類似。整個(gè)evacuation pause階段的日志非常大,為了簡單化,我們把和全年輕代模式evacuation pause無關(guān)的日志省略掉了。在對(duì)并行階段進(jìn)行詳細(xì)介紹之后我們?cè)賮斫忉屵@些被省略的日志。另外,考慮到日志記錄的大小,這里把并行階段和其他階段的詳細(xì)日志都剝離到獨(dú)立的部分。

0.134: [GC pause (G1 Evacuation Pause) (young), 0.0144119 secs]1
[Parallel Time: 13.9 ms, GC Workers: 8]2
3
[Code Root Fixup: 0.0 ms]4
[Code Root Purge: 0.0 ms]5
[Clear CT: 0.1 ms]
[Other: 0.4 ms]6
7
[Eden: 24.0M(24.0M)->0.0B(13.0M) 8Survivors: 0.0B->3072.0K 9Heap: 24.0M(256.0M)->21.9M(256.0M)]10
[Times: user=0.04 sys=0.04, real=0.02 secs] 11

  1. 0.134: [GC pause (G1 Evacuation Pause) (young), 0.0144119 secs]:evacuation pause開始于JVM啟動(dòng)之后0.134秒,持續(xù)了0.0144119秒
  2. [Parallel Time: 13.9 ms, GC Workers: 8]:下列活動(dòng)用8個(gè)并發(fā)工作線程執(zhí)行,總共花了13.9ms(clock time)
  3. 這里為了簡化,省略了具體的活動(dòng)內(nèi)容,具體見后面章節(jié)
  4. [Code Root Fixup: 0.0 ms]:釋放用來管理并行活動(dòng)的數(shù)據(jù)結(jié)構(gòu),這個(gè)值一般都接近于0。這個(gè)操作是順序的
  5. [Code Root Purge: 0.0 ms]:清除更多的數(shù)據(jù)結(jié)構(gòu),這個(gè)操作也非???,但不一定是0。這個(gè)操作也是順序的
  6. [Other: 0.4 ms]:其他各種活動(dòng)耗時(shí),很多都是并行的
  7. 具體見后面章節(jié)
  8. [Eden: 24.0M(24.0M)->0.0B(13.0M):執(zhí)行前后伊甸區(qū)的使用量和總?cè)萘?/li>
  9. Survivors: 0.0B->3072.0K:執(zhí)行前后存活區(qū)的使用量
  10. Heap: 24.0M(256.0M)->21.9M(256.0M)]:執(zhí)行前后堆的使用量和總?cè)萘?/li>
  11. [Times: user=0.04 sys=0.04, real=0.02 secs]:GC事件的時(shí)間開銷,分三個(gè)類別
  • user:整個(gè)過程GC耗費(fèi)的全部CPU時(shí)間
  • sys:系統(tǒng)耗費(fèi)時(shí)間,包括系統(tǒng)調(diào)用或者等待系統(tǒng)事件
  • real:應(yīng)用程序因GC被停止的時(shí)間。由于GC的活動(dòng)是并行的,real time接近于(user time + sys time)/GC線程數(shù),這里使用了8個(gè)線程。因?yàn)槟承┗顒?dòng)不能并行,所以這個(gè)值會(huì)稍稍大一點(diǎn)。

多個(gè)專屬GC工作線程負(fù)責(zé)執(zhí)行大部分耗時(shí)操作,具體內(nèi)容如下:

[Parallel Time: 13.9 ms, GC Workers: 8]1
[GC Worker Start (ms)2: Min: 134.0, Avg: 134.1, Max: 134.1, Diff: 0.1]

[Ext Root Scanning (ms)<sup>3</sup>: 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)<sup>4</sup>: Min: 0.0, Avg: 0.0, Max: 0.2, Diff: 0.2, Sum: 0.2]
[Object Copy (ms)<sup>5</sup>: Min: 10.8, Avg: 12.1, Max: 12.6, Diff: 1.9, Sum: 96.5]
[Termination (ms)<sup>6</sup>: Min: 0.8, Avg: 1.5, Max: 2.8, Diff: 1.9, Sum: 12.2]
    [Termination Attempts<sup>7</sup>: Min: 173, Avg: 293.2, Max: 362, Diff: 189, Sum: 2346]
[GC Worker Other (ms)<sup>8</sup>: Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
GC Worker Total (ms)<sup>9</sup>: Min: 13.7, Avg: 13.8, Max: 13.8, Diff: 0.1, Sum: 110.2]
[GC Worker End (ms)<sup>10</sup>: Min: 147.8, Avg: 147.8, Max: 147.8, Diff: 0.0]
  1. [Parallel Time: 13.9 ms, GC Workers: 8]:下列活動(dòng)用8個(gè)并發(fā)工作線程執(zhí)行,總共花了13.9ms(clock time)
  2. [GC Worker Start (ms):工作線程開始執(zhí)行操作的時(shí)間,應(yīng)該和停頓的開始時(shí)間一致。如果最小和最大差距太大,可能意味著使用了太多線程,或者機(jī)器上有其他進(jìn)程和JVM的GC進(jìn)程爭搶CPU資源
  3. [Ext Root Scanning (ms):掃描非堆的GC roots所耗時(shí)間,包括類加載器,JNI引用,JVM系統(tǒng)根等,除了sum是cpu time,其他都是clock time
  4. [Code Root Scanning (ms):掃描來自實(shí)際代碼的GC roots,比如本地變量等
  5. [Object Copy (ms):從收集區(qū)域塊拷貝活對(duì)象所耗時(shí)間
  6. [Termination (ms):工作線程確信所有工作已經(jīng)做完,可以安全停止所耗時(shí)間
  7. [Termination Attempts:工作線程嘗試中止的次數(shù),如果還有未完成的工作,就是一次失敗的中止嘗試
  8. [GC Worker Other (ms):其他各種活動(dòng)耗時(shí)
  9. GC Worker Total (ms):GC工作線程所耗時(shí)間總和
  10. [GC Worker End (ms):工作線程完成工作的時(shí)間,通常應(yīng)該時(shí)間差不多,否則意味著太多線程堵塞或者有其他進(jìn)程爭搶CPU資源

另外,在該階段還有一些其他活動(dòng)在執(zhí)行。這里只介紹部分內(nèi)容,其他的放在后面章節(jié)。

[Other: 0.4 ms]1
[Choose CSet: 0.0 ms]
[Ref Proc: 0.2 ms]2
[Ref Enq: 0.0 ms]3
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]4

  1. [Other: 0.4 ms]:其他各種活動(dòng)耗時(shí),很多都是并行的
  2. [Ref Proc: 0.2 ms]:處理非強(qiáng)引用耗時(shí):決定是清除它們還是無視它們
  3. [Ref Enq: 0.0 ms]:待處理的非強(qiáng)引用入對(duì)應(yīng)隊(duì)列所耗時(shí)間
  4. [Free CSet: 0.0 ms]:返回收集集合中已經(jīng)釋放區(qū)域塊所耗時(shí)間

并發(fā)標(biāo)記

G1垃圾收集器借鑒了CMS的很多理念。所以在繼續(xù)之前確保你對(duì)CMS的這些概念有很好的理解。盡管G1和CMS有很多不同,但是并發(fā)標(biāo)記階段的目的非常類似。G1并發(fā)標(biāo)記使用SATB(Snapshot-At-The-Beginning)的方法,對(duì)所有在標(biāo)記開始那一刻活著的對(duì)象進(jìn)行標(biāo)記,即使后來它變成垃圾。根據(jù)這些信息可以構(gòu)建每個(gè)區(qū)域塊的活對(duì)象狀態(tài)信息,這樣就可以很高效的確定收集集合。

這個(gè)信息也用于老生代的GC。如果通過標(biāo)記發(fā)現(xiàn)某個(gè)區(qū)域塊只包含垃圾,或者在對(duì)老生代(既包含垃圾也包含活對(duì)象)做evacuation pause的STW期間,并發(fā)標(biāo)記可以完全并發(fā)執(zhí)行。

當(dāng)堆內(nèi)存使用空間足夠大就會(huì)觸發(fā)并發(fā)標(biāo)記。默認(rèn)是45%,這個(gè)值可以通過InitiatingHeapOccupancyPercent進(jìn)行調(diào)整。和CMS類似,G1中的并發(fā)標(biāo)記也包含很多階段,這些階段有些是完全并發(fā),有些需要停止應(yīng)用程序。

Phase 1: Initial Mark。 標(biāo)記所有可以從GC roots直達(dá)的對(duì)象。在CMS里,這個(gè)階段需要單獨(dú)的STW,但是在G1里,它只是evacuation pause的附帶停頓,所以它的開銷很小。在日志里面的表現(xiàn)就是在evacuation pause日志首行帶有Initial Mark字樣。

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

Phase 2: Root Region Scan。 標(biāo)記所有可以從所謂的根區(qū)域塊可達(dá)的活對(duì)象。例如,那些不是空。因?yàn)樵诓l(fā)標(biāo)記過程中移動(dòng)對(duì)象會(huì)有問題,這個(gè)過程必須在下一次evacuation pause開始之前完成。如果它要提前開始,就需要提前中止root region scan,并等到它結(jié)束。在目前的實(shí)現(xiàn)中,根區(qū)域塊是存活區(qū):他們是年輕代里面的對(duì)象,必定在下一次evacuation pause中被回收。

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

Phase 3:Concurrent Mark。 跟CMS類似:遍歷對(duì)象圖,在一個(gè)特殊的位圖里面標(biāo)記訪問的對(duì)象。為了確保滿足SATB的要求,G1要求應(yīng)用程序?qū)?duì)象圖的并發(fā)更新要留下用來標(biāo)記的舊引用。這是通過Pre-Write barriers實(shí)現(xiàn)的(注意不要跟下文提到的Post-Write barriers以及多線程編程里面的memory barriers混淆概念):在G1并發(fā)標(biāo)記執(zhí)行過程中,對(duì)任何對(duì)象的寫入,把舊引用存儲(chǔ)到日志緩存里面,這些內(nèi)容到時(shí)候會(huì)被并發(fā)標(biāo)記線程處理的。

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

Phase 4:Remark。 這個(gè)跟CMS的最終標(biāo)記類似,需要STW停頓,用來終結(jié)整個(gè)標(biāo)記階段。G1停止應(yīng)用程序,不讓它們繼續(xù)產(chǎn)生并發(fā)更新日志,然后處理完剩下的日志,標(biāo)記所有在并發(fā)標(biāo)記開始時(shí)刻是活著的所有未標(biāo)記對(duì)象。該階段還會(huì)處理一些額外的清除操作,比如引用處理(請(qǐng)參照前面的evacuation pause日志)或者類卸載。

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]

Phase 5:Cleanup。 并發(fā)標(biāo)記的最終階段,為即將到來的evacuation pause做準(zhǔn)備:計(jì)算堆區(qū)域塊所有活對(duì)象,根據(jù)期望的GC效益來排序這些區(qū)域塊。為了下次并發(fā)標(biāo)記,還需要做一些清理工作來維護(hù)內(nèi)部數(shù)據(jù)結(jié)構(gòu)的狀態(tài)。

最后還有一件重要的事是,沒有包含活對(duì)象的區(qū)域塊會(huì)在這個(gè)階段被回收。這個(gè)階段有些部分是并發(fā),比如空區(qū)域塊回收,大部分的活性計(jì)算,但也需要一個(gè)短暫的STW停頓來結(jié)束整個(gè)過程,以防止應(yīng)用程序干預(yù)。這個(gè)停頓的日志如下:

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

當(dāng)出現(xiàn)某些堆區(qū)域塊只包含垃圾的時(shí)候,日志格式稍微有點(diǎn)不同:

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:混合模式

如果并發(fā)清除操作能夠釋放掉老生代的整個(gè)區(qū)域塊那是最好不過了,但是經(jīng)常不是這樣。在并發(fā)標(biāo)記結(jié)束之后,G1會(huì)在混合模式下工作,不僅僅是從年輕代區(qū)域收集垃圾,也會(huì)把一些老生代區(qū)域捎帶放到收集集合中。

混合模式的evacuation pause一般并不會(huì)緊跟著并發(fā)標(biāo)記階段。是否要進(jìn)行evacuation pause,受到很多規(guī)則和直覺影響。比如,如果在并發(fā)標(biāo)記階段已經(jīng)釋放了大部分老生代區(qū)域,就沒必要立馬進(jìn)行evacuation pause了。

因此有可能在并發(fā)標(biāo)記結(jié)束和混合模式evacuation pause之間,有很多次全年輕代模式evacuation pause。

被加入到收集集合中的老生代區(qū)域塊個(gè)數(shù),加入的順序也是基于很多規(guī)則的。包括應(yīng)用的軟實(shí)時(shí)性能目標(biāo),并發(fā)標(biāo)記過程中收集到的活性數(shù)據(jù)和gc效益數(shù)據(jù),JVM參數(shù)配置等。混合模式的evacuation pause過程跟全年輕代模式的evacuation pause大部分一樣,這里我們主要介紹一下remembered sets概念。

remembered sets使得對(duì)不同堆區(qū)域進(jìn)行獨(dú)立GC成為可能。比如,如果對(duì)區(qū)域A,B,C進(jìn)行GC,我們需要知道是否有從區(qū)域D,E來的對(duì)象引用,這對(duì)影響到對(duì)一個(gè)對(duì)象是否是活著的判斷。但是遍歷整個(gè)堆對(duì)象圖非常耗時(shí),這也不符合增量回收的初衷,因此這里需要進(jìn)行優(yōu)化。類似于其他垃圾收集算法使用卡片表來實(shí)現(xiàn)對(duì)年輕代的獨(dú)立回收,在G1這里就是remembered sets。

如下圖所示,每一個(gè)區(qū)域塊都有一個(gè)remembered set,里面記錄了所有來自外部的引用,這些引用將被認(rèn)為是GC roots的補(bǔ)充。注意在并發(fā)標(biāo)記過程中被判定為垃圾的老生代區(qū)域的對(duì)象會(huì)被無視,即使有來自外部的引用指向這些對(duì)象,這個(gè)時(shí)候這些引用方也是垃圾。

g1-03.png

接下來就跟其他垃圾收集器一樣:多個(gè)并行GC線程找出來哪些對(duì)象是活的,哪些是垃圾。

g1-04.png

最后,活對(duì)象被拷貝到存活區(qū),如果需要的話會(huì)創(chuàng)建一個(gè)新的存活區(qū)。這樣空區(qū)域被釋放了,可以用來存儲(chǔ)新對(duì)象了。

g1-05.png

為了維護(hù)這個(gè)remembered set,在應(yīng)用程序運(yùn)行過程中,當(dāng)需要對(duì)某個(gè)字段進(jìn)行寫操作就會(huì)觸發(fā)一個(gè)Post-Write Barrier。如果最終的引用是跨區(qū)的,比如從一個(gè)區(qū)域指向另外一個(gè)區(qū)域,這個(gè)時(shí)候在目標(biāo)區(qū)域的remembered set里面就會(huì)相應(yīng)追加一條記錄。為了減少Write Barrier的開銷,把這個(gè)卡片放入到remembered set里面的操作是異步的,并采取了一些其他的優(yōu)化措施?;緛碚f分幾個(gè)步驟:Write Barrier把臟卡片信息放入到一個(gè)本地緩存,一個(gè)專屬GC線程讀取它再把這個(gè)信息告訴給被引用區(qū)域的remembered set。

混合模式的日志里面會(huì)輸出它自己獨(dú)有的一些信息。

[Update RS (ms)1: Min: 0.7, Avg: 0.8, Max: 0.9, Diff: 0.2, Sum: 6.1]
[Processed Buffers2: Min: 0, Avg: 2.2, Max: 5, Diff: 5, Sum: 18]
[Scan RS (ms)3: Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.8]
[Clear CT: 0.2 ms]4
[Redirty Cards: 0.1 ms]5

  1. [Update RS (ms):因?yàn)閷?duì)RS的操作是并發(fā)的,我們必須確保在實(shí)際收集開始之前要處理完緩存中的卡片。如果這個(gè)數(shù)字很高的話,說明并發(fā)GC線程無法處理這個(gè)負(fù)荷,這可能是因?yàn)榇罅康淖侄涡薷膶?dǎo)致也可能是因?yàn)镃PU資源不夠?qū)е?/li>
  2. [Processed Buffers:每個(gè)工作線程處理本地緩存次數(shù)
  3. [Scan RS (ms):掃描RS里面引用耗時(shí)
  4. [Clear CT: 0.2 ms]:清除卡片表中卡片耗時(shí),就是簡單去除字段的臟數(shù)據(jù)標(biāo)簽
  5. [Redirty Cards: 0.1 ms]:在卡片表中的適當(dāng)位置標(biāo)記臟數(shù)據(jù)耗時(shí),這個(gè)位置是有GC對(duì)堆的修改決定的,比如在對(duì)引用進(jìn)行入隊(duì)列時(shí)

總結(jié)

這節(jié)詳細(xì)全面介紹了G1是如何工作的。當(dāng)然還是有些實(shí)現(xiàn)細(xì)節(jié)是簡要帶過的,比如如何處理humongous objects,整體來講,G1代表了HotSpot里面商用垃圾收集器的最前端技術(shù)成果。隨著Java的版本的不斷發(fā)布,對(duì)G1的優(yōu)化也一直在進(jìn)行。

從中可以看出,G1解決了CMS面臨的很多問題,從停頓的可預(yù)見性到堆內(nèi)存碎片。假如一個(gè)應(yīng)用不受限于CPU利用率,但是對(duì)每個(gè)操作的延遲非常敏感,對(duì)HotSpot用戶來說,G1就是最佳選擇了,特別是當(dāng)運(yùn)行在最新版Java上的應(yīng)用。但是,降低延遲并不是沒有代價(jià)的:因?yàn)橛蓄~外的write barriers和更多活躍的后臺(tái)線程,G1的吞吐量開銷要更大一些。所以,如果應(yīng)用程序更看重吞吐量或者已經(jīng)耗盡了CPU資源,不太在意每次停頓時(shí)長,CMS或者并行算法的垃圾收集器會(huì)更加適合。

要想選擇合適的GC算法和參數(shù)配置,唯一可行的方法就是不停的試錯(cuò)。但是我們會(huì)在下章給出一些基本的指導(dǎo)規(guī)則。

*注意:G1可能成為Java 9的默認(rèn)GC:http://openjdk.java.net/jeps/248 *

Shenandoah

我們已經(jīng)介紹了HotSpot中所有可用的商用GC算法。還有一個(gè)正在開發(fā)中的,被稱作“Ultra-Low-Pause-Time”垃圾收集器。它是專門為大型多核,大內(nèi)存堆的服務(wù)器而設(shè)計(jì)的,其目標(biāo)是在堆內(nèi)存容量在100GB+的時(shí)候,停頓能控制在10ms以內(nèi)。當(dāng)然這個(gè)同樣是以降低應(yīng)用程序吞吐量為代價(jià)的:設(shè)計(jì)者的目標(biāo)是用不超過10%的性能損失換取零GC停頓。

在該算法可用于生產(chǎn)環(huán)境之前,我們這里就不深入介紹具體的算法實(shí)現(xiàn)了,但是它仍然是基于這里講的很多概念的,比如并發(fā)標(biāo)記,增量回收等。當(dāng)然它也有很多不同的實(shí)現(xiàn),它不對(duì)堆內(nèi)存進(jìn)行分代,只有一個(gè)空間。對(duì)的,它不是分代垃圾回收器。這樣它就沒有card tables和remembered sets的概念。它也使用forwarding pointers和Brooks style read barrier來實(shí)現(xiàn)并發(fā)拷貝活對(duì)象,從而降低停頓的次數(shù)和持續(xù)時(shí)間。

更多關(guān)于Shenandoah算法進(jìn)展可以關(guān)注這個(gè)博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容