天天看點

Minor GC、Major GC、Full GC以及Mixed GC之間的差別

堆記憶體劃分為 Eden、Survivor 和 Tenured/Old 空間

Minor GC、Major GC、Full GC以及Mixed GC之間的差別

Minor GC

從年輕代空間(包括 Eden 和 Survivor 區域)回收記憶體被稱為 Minor GC。這一定義既清晰又易于了解。但是,當發生Minor GC事件的時候,有一些有趣的地方需要注意到:

  1. 當 JVM 無法為一個新的對象配置設定空間時會觸發 Minor GC,比如當 Eden 區滿了。是以配置設定率越高,越頻繁執行 Minor GC。
  2. 記憶體池被填滿的時候,其中的内容全部會被複制,指針會從0開始跟蹤空閑記憶體。Eden 和 Survivor 區進行了标記和複制操作,取代了經典的标記、掃描、壓縮、清理操作。是以 Eden 和 Survivor 區不存在記憶體碎片。寫指針總是停留在所使用記憶體池的頂部。
  3. 執行 Minor GC 操作時,不會影響到永久代。從永久代到年輕代的引用被當成 GC roots,從年輕代到永久代的引用在标記階段被直接忽略掉。
  4. 質疑正常的認知,所有的 Minor GC 都會觸發“全世界的暫停(stop-the-world)”,停止應用程式的線程。對于大部分應用程式,停頓導緻的延遲都是可以忽略不計的。其中的真相就是,大部分 Eden 區中的對象都能被認為是垃圾,永遠也不會被複制到 Survivor 區或者老年代空間。如果正好相反,Eden 區大部分新生對象不符合 GC 條件,Minor GC 執行時暫停的時間将會長很多。

是以 Minor GC 的情況就相當清楚了——每次 Minor GC 會清理年輕代的記憶體。

Major GC vs Full GC

大家應該注意到,目前,這些術語無論是在 JVM 規範還是在垃圾收集研究論文中都沒有正式的定義。但是我們一看就知道這些在我們已經知道的基礎之上做出的定義是正确的,Minor GC 清理年輕帶記憶體應該被設計得簡單:

  • Major GC 是清理老年代。
  • Full GC 是清理整個堆空間—包括年輕代、老年代及永久代(中繼資料空間)。

很不幸,實際上它還有點複雜且令人困惑。首先,許多 Major GC 是由 Minor GC 觸發的,是以很多情況下将這兩種 GC 分離是不太可能的。另一方面,許多現代垃圾收集機制會清理部分永久代空間,是以使用“cleaning”一詞隻是部分正确。

這使得我們不用去關心到底是叫 Major GC 還是 Full GC,大家應該關注目前的 GC 是否停止了所有應用程式的線程,還是能夠并發的處理而不用停掉應用程式的線程。

這種混亂甚至内置到 JVM 标準工具。下面一個例子很好的解釋了我的意思。讓我們比較兩個不同的工具 Concurrent Mark 和 Sweep collector (-XX:+UseConcMarkSweepGC)在 JVM 中運作時輸出的跟蹤記錄。

第一次嘗試通過 jstat 輸出:

my-precious: me$ jstat -gc -t 4235 1s

Time S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
 5.7 34048.0 34048.0  0.0   34048.0 272640.0 194699.7 1756416.0   181419.9  18304.0 17865.1 2688.0 2497.6      3    0.275   0      0.000    0.275
 6.7 34048.0 34048.0 34048.0  0.0   272640.0 247555.4 1756416.0   263447.9  18816.0 18123.3 2688.0 2523.1      4    0.359   0      0.000    0.359
 7.7 34048.0 34048.0  0.0   34048.0 272640.0 257729.3 1756416.0   345109.8  19072.0 18396.6 2688.0 2550.3      5    0.451   0      0.000    0.451
 8.7 34048.0 34048.0 34048.0 34048.0 272640.0 272640.0 1756416.0  444982.5  19456.0 18681.3 2816.0 2575.8      7    0.550   0      0.000    0.550
 9.7 34048.0 34048.0 34046.7  0.0   272640.0 16777.0  1756416.0   587906.3  20096.0 19235.1 2944.0 2631.8      8    0.720   0      0.000    0.720
10.7 34048.0 34048.0  0.0   34046.2 272640.0 80171.6  1756416.0   664913.4  20352.0 19495.9 2944.0 2657.4      9    0.810   0      0.000    0.810
11.7 34048.0 34048.0 34048.0  0.0   272640.0 129480.8 1756416.0   745100.2  20608.0 19704.5 2944.0 2678.4     10    0.896   0      0.000    0.896
12.7 34048.0 34048.0  0.0   34046.6 272640.0 164070.7 1756416.0   822073.7  20992.0 19937.1 3072.0 2702.8     11    0.978   0      0.000    0.978
13.7 34048.0 34048.0 34048.0  0.0   272640.0 211949.9 1756416.0   897364.4  21248.0 20179.6 3072.0 2728.1     12    1.087   1      0.004    1.091
14.7 34048.0 34048.0  0.0   34047.1 272640.0 245801.5 1756416.0   597362.6  21504.0 20390.6 3072.0 2750.3     13    1.183   2      0.050    1.233
15.7 34048.0 34048.0  0.0   34048.0 272640.0 21474.1  1756416.0   757347.0  22012.0 20792.0 3200.0 2791.0     15    1.336   2      0.050    1.386
16.7 34048.0 34048.0 34047.0  0.0   272640.0 48378.0  1756416.0   838594.4  22268.0 21003.5 3200.0 2813.2     16    1.433   2      0.050    1.484
           

這個片段是 JVM 啟動後第17秒提取的。基于該資訊,我們可以得出這樣的結果,運作了12次 Minor GC、2次 Full GC,時間總跨度為50毫秒。通過 jconsole 或者 jvisualvm 這樣的基于GUI的工具你能得到同樣的結果。 

java -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC eu.plumbr.demo.GarbageProducer

3.157: [GC (Allocation Failure) 3.157: [ParNew: 272640K->34048K(306688K), 0.0844702 secs] 272640K->69574K(2063104K), 0.0845560 secs] [Times: user=0.23 sys=0.03, real=0.09 secs] 
4.092: [GC (Allocation Failure) 4.092: [ParNew: 306688K->34048K(306688K), 0.1013723 secs] 342214K->136584K(2063104K), 0.1014307 secs] [Times: user=0.25 sys=0.05, real=0.10 secs] 
... cut for brevity ...
11.292: [GC (Allocation Failure) 11.292: [ParNew: 306686K->34048K(306688K), 0.0857219 secs] 971599K->779148K(2063104K), 0.0857875 secs] [Times: user=0.26 sys=0.04, real=0.09 secs] 
12.140: [GC (Allocation Failure) 12.140: [ParNew: 306688K->34046K(306688K), 0.0821774 secs] 1051788K->856120K(2063104K), 0.0822400 secs] [Times: user=0.25 sys=0.03, real=0.08 secs] 
12.989: [GC (Allocation Failure) 12.989: [ParNew: 306686K->34048K(306688K), 0.1086667 secs] 1128760K->931412K(2063104K), 0.1087416 secs] [Times: user=0.24 sys=0.04, real=0.11 secs] 
13.098: [GC (CMS Initial Mark) [1 CMS-initial-mark: 897364K(1756416K)] 936667K(2063104K), 0.0041705 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
13.102: [CMS-concurrent-mark-start]
13.341: [CMS-concurrent-mark: 0.238/0.238 secs] [Times: user=0.36 sys=0.01, real=0.24 secs] 
13.341: [CMS-concurrent-preclean-start]
13.350: [CMS-concurrent-preclean: 0.009/0.009 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
13.350: [CMS-concurrent-abortable-preclean-start]
13.878: [GC (Allocation Failure) 13.878: [ParNew: 306688K->34047K(306688K), 0.0960456 secs] 1204052K->1010638K(2063104K), 0.0961542 secs] [Times: user=0.29 sys=0.04, real=0.09 secs] 
14.366: [CMS-concurrent-abortable-preclean: 0.917/1.016 secs] [Times: user=2.22 sys=0.07, real=1.01 secs] 
14.366: [GC (CMS Final Remark) [YG occupancy: 182593 K (306688 K)]14.366: [Rescan (parallel) , 0.0291598 secs]14.395: [weak refs processing, 0.0000232 secs]14.395: [class unloading, 0.0117661 secs]14.407: [scrub symbol table, 0.0015323 secs]14.409: [scrub string table, 0.0003221 secs][1 CMS-remark: 976591K(1756416K)] 1159184K(2063104K), 0.0462010 secs] [Times: user=0.14 sys=0.00, real=0.05 secs] 
14.412: [CMS-concurrent-sweep-start]
14.633: [CMS-concurrent-sweep: 0.221/0.221 secs] [Times: user=0.37 sys=0.00, real=0.22 secs] 
14.633: [CMS-concurrent-reset-start]
14.636: [CMS-concurrent-reset: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
           

在點頭同意這個結論之前,讓我們看看來自同一個 JVM 啟動收集的垃圾收集日志的輸出。顯然- XX:+ PrintGCDetails 告訴我們一個不同且更詳細的故事:

基于這些資訊,我們可以看到12次 Minor GC 後開始有些和上面不一樣了。沒有運作兩次 Full GC,這不同的地方在于單個 GC 在永久代中不同階段運作了兩次:

  • 最初的标記階段,用了0.0041705秒也就是4ms左右。這個階段會暫停“全世界( stop-the-world)”的事件,停止所有應用程式的線程,然後開始标記。
  • 并行執行标記和清洗階段。這些都是和應用程式線程并行的。
  • 最後 Remark 階段,花費了0.0462010秒約46ms。這個階段會再次暫停所有的事件。
  • 并行執行清理操作。正如其名,此階段也是并行的,不會停止其他線程。

是以,正如我們從垃圾回收日志中所看到的那樣,實際上隻是執行了 Major GC 去清理老年代空間而已,而不是執行了兩次 Full GC。

如果你是後期做決 定的話,那麼由 jstat 提供的資料會引導你做出正确的決策。它正确列出的兩個暫停所有事件的情況,導緻所有線程停止了共計50ms。但是如果你試圖優化吞吐量,你會被誤導的。清 單隻列出了回收初始标記和最終 Remark 階段,jstat的輸出看不到那些并發完成的工作。

Mixed GC

是在G1收集器中獨有的,用于收集整個young gen以及部分old gen的GC。

結論

  • Minor GC 從年輕代空間(包括 Eden 和 Survivor 區域)回收記憶體。
  • Major GC 是清理老年代。
  • Mixed GC 是在G1收集器中獨有的,用于收集整個young gen以及部分old gen的GC。
  • Full GC 是清理整個堆空間—包括年輕代、老年代及永久代(中繼資料空間)。

考慮到這種情況,最好避免以 Minor、Major、Full GC 這種方式來思考問題。而應該監控應用延遲或者吞吐量,然後将 GC 事件和結果聯系起來。

随着這些 GC 事件的發生,你需要額外的關注某些資訊,GC 事件是強制所有應用程式線程停止了還是并行的處理了部分事件。

針對HotSpot VM的實作,它裡面的GC其實準确分類隻有兩大種: 

  • Partial GC:并不收集整個GC堆的模式
    • Young GC:隻收集young gen的GC
    • Old GC:隻收集old gen的GC。隻有CMS的concurrent collection是這個模式
    • Mixed GC:收集整個young gen以及部分old gen的GC。隻有G1有這個模式
  • Full GC:收集整個堆,包括young gen、old gen、perm gen(如果存在的話)等所有部分的模式。

Major GC通常是跟full GC是等價的,收集整個GC堆。但因為HotSpot VM發展了這麼多年,外界對各種名詞的解讀已經完全混亂了,當有人說“major GC”的時候一定要問清楚他想要指的是上面的full GC還是old gen。 

最簡單的分代式GC政策,按HotSpot VM的serial GC的實作來看,觸發條件是: 

  • young GC:當young gen中的eden區配置設定滿的時候觸發。注意young GC中有部分存活對象會晉升到old gen,是以young GC後old gen的占用量通常會有所升高。
  • full GC:當準備要觸發一次young GC時,如果發現統計資料說之前young GC的平均晉升大小比目前old gen剩餘的空間大,則不會觸發young GC而是轉為觸發full GC(因為HotSpot VM的GC裡,除了CMS的concurrent collection之外,其它能收集old gen的GC都會同時收集整個GC堆,包括young gen,是以不需要事先觸發一次單獨的young GC);或者,如果有perm gen的話,要在perm gen配置設定空間但已經沒有足夠空間時,也要觸發一次full GC;或者System.gc()、heap dump帶GC,預設也是觸發full GC。

HotSpot VM裡其它非并發GC的觸發條件複雜一些,不過大緻的原理與上面說的其實一樣。 當然也總有例外。Parallel Scavenge(-XX:+UseParallelGC)架構下,預設是在要觸發full GC前先執行一次young GC,并且兩次GC之間能讓應用程式稍微運作一小下,以期降低full GC的暫停時間(因為young GC會盡量清理了young gen的死對象,減少了full GC的工作量)。這是HotSpot VM裡的奇葩嗯。 并發GC的觸發條件就不太一樣。以CMS GC為例,它主要是定時去檢查old gen的使用量,當使用量超過了觸發比例就會啟動一次CMS GC,對old gen做并發收集。