1. 程式人生 > >JVM垃圾回收(四)- GC算法:實現(1)

JVM垃圾回收(四)- GC算法:實現(1)

並行 ctime 配置 使用情況 ava 第一個 中標 算法 bsp

GC算法:實現

上面我們介紹了GC算法中的核心概念,接下來我們看一下JVM裏的具體實現。首先必須了解的一個重要的事實是:對於大部分的JVM來說,兩種不同的GC算法是必須的,一個是清理Young Generation的算法,另一種是清理Old Generation的算法。

在JVM裏有各種各樣的這種內置算法,如果你沒有特別指定GC算法,則會使用一個默認的、適應當前平臺(platform-specific)的算法。接下來我們會解釋每種算法的工作原理。

下面的列表提供了一個快速的預覽,關於哪些算法可能被結合使用。不過需要註意的是,它僅適用於Java 8,對應Java 8 之前的版本,可能稍有不同。

技術分享圖片

如果上表看起來很復雜,不要慌。在實際使用中,基本可以歸結為上面表中標粗的部分。剩下的不是被棄用,就是不被支持,或是在實際場景中不實用。所以,下面我們僅僅會討論下面的幾種組合:

  1. Serial GC for both the Young and Old generation
  2. Parallel GC for both the Young and Old generation
  3. Parallel New for Young + Concurrent Mark and Sweep (CMS) for the Old Generation
  4. G1 in case of which the generation are not separated between the Young and Old

Serial GC

這種垃圾回收器在Young Generation使用mark-copy,在Old Generation使用mark-sweep-compact。正如它的名字一樣,這兩種收集器均是單線程的收集器,無法與當前的任務並行工作。這兩種收集器均會觸發stop-the-world pauses,暫時停止所有應用線程。

這種GC算法無法使用當前主流硬件上多核CPU的優點,不管有多少可用的CPU核數,JVM在GC階段僅會使用一個核。可以通過指定以下配置應用此機制:

java -XX:+UseSerialGC com.company.testclass

這個選項僅推薦給:

  1. JVM中僅有幾百MB的堆大小
  2. 運行的環境是單核CPU

對於大部分的服務端部署來說,很少會使用這種模式,因為大部分服務端的應用一般部署在多核平臺,並不適合Serial GC的使用場景,會造成服務器資源的浪費。接下來我們看一下如果使用Serial GC 的話,那GC 收集器的日誌會是什麽形式。首先我們在JVM下開啟GC日誌:

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

日誌的輸出類似以下內容:

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,一個是清理Young Generation,另一個是清理整個堆。我們首先分析第一個在Young Generation發生的GC。

Minor GC

下面的日誌片段包括了GC清理Young Generation時的一些信息:

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事件發生的時間

2. 151.126 : 相對於JVM的啟動時間,GC事件發生的時間,以秒為單位

3. GC: GC類型的標誌,用於區別是 Minor GC 還是 Full GC。這次顯示的是一次 Minor GC

4. Allocation Failure :GC發生的原因。在這個日誌中,表示的是是由於Young Generation 裏的任何區域均無法滿足一個(對某個數據結構的)空間分配

5. DefNew :使用的 GC 收集器名稱。這個縮略名表示的是單線程的、mark-copy、stop-the-world 垃圾回收器,用於清理Young Generation

6. 629119K->69888K :Young Generation的使用情況,分為 GC 前以及 GC 後

7. (629120K) :Young Generation 的總大小

8. 1619346K->1273247K :堆內存使用的總大小,分為GC前與GC後

9. (2027264K) :堆內存總可用大小

10. 0.0585007 secs :GC事件的持續總長時間,以秒為單位

11. [Times: user=0.06 sys=0.00, real=0.06 secs] :GC事件的持續時間,從三種不同的類別衡量:

A. user:在GC 階段,GC 線程消耗的整個CPU時間

B. sys:OS 調用消耗的時間,或是等待系統事件的時間

C. real:應用停止的時間。因為 Serial GC 一直使用的是單線程,所以這裏 real time 等於 user 與 system 時間的總和

從上面的片段,我們可以精確地了解到在 GC 事件時,JVM內部的內存消耗情況。在這次回收前,heap 使用了總共 1,619,346K 大小的內存,其中 Young Generation 一共占了 629,120K 內存。基於此,我們可以計算出 Old Generation 使用量為 990,227K內存。

另一方面,我們也可以看到,在回收之後,Young Generation 的使用量降了 559,231K,但是整個heap 的使用量僅降了346,099K,由此可以推測出,有 213,132K 的對象從 Young Generation 被提升到了 Old Generation。

這次 GC 事件前後,內存的分布,也可以通過下圖表示:

技術分享圖片

Full GC

在討論了第一個 Minor GC 事件後,我們再來看看第二個 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->755802K 5(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事件開始的時間

2. 172.829 : 相對於JVM的啟動時間,GC事件發生的時間,以秒為單位

3. [DefNew: 629120K->629120K(629120K), 0.0000372 secs :類似上一個例子(由於 Allocation Failure觸發的一個minor GC),這次對 Young Generation 的回收也是同樣由 DefNew 回收器完成。它將 Young Generation的使用量由 629,120K 降為 0。需要註意的是:這裏的日誌打印有問題,由於一個存在bug的行為,導致它打印的日誌為 Young Generation 使用為滿的狀態。這次回收耗時 0.0000372 秒

4.Tenured :清理 Old 空間時使用的 GC 收集器名稱。這裏 Tenured 表示GC使用了一個單線程的、stop-the-world、mark-sweep-compact 垃圾回收器

5. 1203359K->755802K :在 GC 事件前後,Old Generation 使用的空間大小

6. (1398144K) :Old Generation 空間的總共大小

7. 0.1855567 secs :清理 Old Generation 的時間

8. 1832479K->755802K :清理 Young 以及 Old Generation 前後,整個 heap 使用的內存大小

9. (2027264K) :JVM 可用的 heap 大小

10. [Metaspace: 6741K->6741K(1056768K)] :類似 Metaspace 空間回收的信息,正如日誌打印的,這次回收中,沒有Metaspace的垃圾被回收

11. [Times: user=0.18 sys=0.00, real=0.18 secs] :GC事件的持續時間,從三種不同的類別衡量:

A. user:在GC 階段,GC 線程消耗的整個CPU時間

B. sys:OS 調用消耗的時間,或是等待系統事件的時間

C. real:應用停止的時間。因為 Serial GC 一直使用的是單線程,所以這裏 real time 等於 user 與 system 時間的總和

Full GC 與 Minor GC 的不同點顯而易見:在 GC 事件中,除了對 Young Generation 做了垃圾回收外,Old Generation 與 Metaspace 也被做了清理。在這個例子中,在 GC 事件前後,內存的分布可如下如表示:

技術分享圖片

Parallel GC

這種GC收集器的組合(對 Young 與 Old 使用的兩種 GC收集器),在 Young Generation 中使用 mark-copy,在Old Generation中使用mark-sweep-compact。對 Young 與 Old Generation的收集均會觸發 stop-the-world 事件,暫停應用的所有線程,以運行 GC。兩個收集器均會以多線程的方式運行 mark-copy / mark-sweep-compact,所以它的名字為 ‘Parallel’。使用並行的方式,可以明顯減少GC的時間。在GC時,使用多少個線程也可以通過參數指定:-XX:ParallelGCThreads=NNN。默認的值是:機器的CPU核數。在啟動JVM時使用以下任一配置即可啟用ParallelGC:

java -XX:+UseParallelGC com.company.MyClass

java -XX:+UseParallelOldGC com.company.MyClass

java -XX:+UseParallelGC -XX:+UseParallelOldGC com.compay.MyClass

Parallel 垃圾收集器適用與多核機器,所以如果你的主要目標是為了提高吞吐,則Parallel GC是一個較好的選擇。可以獲得高吞吐是由於此方法高效地使用了系統資源:

  1. 在收集過程中,所有CPU 核均會並行回收垃圾,所以應用暫停時間會更短
  2. 在GC輪數之間,不會有收集器消耗任何資源

另一方面,由於在GC中所有的階段在運行時不可被打斷,所以在你的應用線程暫停時,這些收集器仍容易受到long pause的影響。所以,如果Latency是你需要優先考慮的目標,則你可以考慮下一個垃圾收集器組合。

下面我們看一下使用Parallel GC時,日誌輸出的信息。下面是一個 minor 和一個 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

第一條表示了在 Young Generation裏發生的一個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事件發生的時間

2. 116.115 : 相對於JVM的啟動時間,GC事件發生的時間,以秒為單位

3. GC: GC類型的標誌,用於區別是 Minor GC 還是 Full GC。這次顯示的是一次 Minor GC

4. Allocation Failure :GC發生的原因。在這個日誌中,表示的是是由於Young Generation 裏的任何區域均無法滿足一個(對某個數據結構的)空間分配

5. PSYoungGen:使用的 GC 收集器名稱。這裏表示的是一個並行的、mark-copy、stop-the-world 垃圾回收器被用於清理Young Generation

6. 2694440K->1305132K:Young Generation的使用情況,分為 GC 前以及 GC 後

7. (2796544K):Young Generation 的總大小

8. 9556775K->8438926K :堆內存使用的總大小,分為GC前與GC後

9. (11185152K):堆內存總可用大小

10. 0.2406675 secs:GC事件的持續總長時間,以秒為單位

11. [Times: user=1.77 sys=0.01, real=0.24 secs] :GC事件的持續時間,從三種不同的類別衡量:

A. user:在GC 階段,GC 線程消耗的整個CPU時間

B. sys:OS 調用消耗的時間,或是等待系統事件的時間

C. real:應用停止的時間。對於 Parallel GC 來說,它的值應該接近於(user time + system time)/ GC 收集器使用的 CPU 線程數。在這個例子中,GC 收集器使用的是 8 個線程。不過需要註意的是,由於一些活動並不會被並行執行,所以它的真實值會超過一定的比率。

從上面的日誌可以看到,在 GC 事件前,整個 heap 中消耗的內存為 9,556,775K,其中 Young Generation 消耗了2,694,440K,也就是說 Old Generation 使用了 6,862,335K。在 GC 後,Young Generation 的使用量降了 1,389,308K,但是整個 heap 的使用量僅降了 1,117,849K。也就是說,有 271,459K 從 Young Generation 提升到了 Old Generation。

技術分享圖片

Full GC

下面我們繼續看下一行 GC 日誌,看看 GC 是如何清理整個 heap 內存的:

2015-05-26T14:27:41.155-02001:116.3562:[Full GC3 (Ergonomics4)[PSYoungGen: 1305132K->0K(2796544K)]5[ParOldGen6:7133794K->6597672K 7(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-3 省略

4. Ergonomics:GC 事件發生的原因。這裏表示 JVM 的內部功效決定這時候需要做垃圾回收

5. [PSYoungGen: 1305132K->0K(2796544K)]:與之前的例子類似,一個名為“PSYoungGen”的、並行的、mark-copy、stop-the-world GC 回收器被用於清理 Young Generation。Young Generation 的使用情況由 1,305,132K 降為了 0,因為一般一次 Full GC 經常會將 Young GC 完全清理掉。

6. ParOldGen:用於清理 Old Generation 的收集器類型。在這個例子中,一個名為 ParOldGen 的、並行的、mark-sweep-compact、stop-the-world 垃圾回收器被用於清理 Old Generation。

7. 7133794K->6597672K :在 GC 事件前後,Old Generation 使用的空間大小

8. (8388608K) :Old Generation 空間的總共大小

9. 8438926K->6597672K:清理 Young 以及 Old Generation 前後,整個 heap 使用的內存大小

10. (11185152K) :JVM 可用的 heap 大小

11. [Metaspace: 6745K->6745K(1056768K)] :類似 Metaspace 空間回收的信息,正如日誌打印的,這次事件中,沒有Metaspace的垃圾被回收

12. 0.9158801 secs:GC 事件持續的時間

13. [Times: user=4.49 sys=0.64, real=0.92 secs] GC事件的持續時間,從三種不同的類別衡量:

A. user:在GC 階段,GC 線程消耗的整個CPU時間

B. sys:OS 調用消耗的時間,或是等待系統事件的時間

C. real:應用停止的時間。對於 Parallel GC 來說,它的值應該接近於(user time + system time)/ GC 收集器使用的 CPU 線程數。在這個例子中,GC 收集器使用的是 8 個線程。不過需要註意的是,由於一些活動並不會被並行執行,所以它的真實值會超過一定的比率。

同樣,Full GC 與 Minor GC 的區別較為明顯:除了清理 Young Generation,Old Generation 與 Metaspace 也會被清理。在這個例子中,在 GC 事件前後,內存的分布可如下如表示:

技術分享圖片

References:

https://plumbr.io/handbook/garbage-collection-algorithms-implementations

JVM垃圾回收(四)- GC算法:實現(1)