天天看點

JVM 垃圾回收 Minor gc vs Major gc vs Full gc

本文是本人學習三種GC(minor,major,full)中獲得的精華資訊。

整合 RednaxelaFX的知乎回答(https://www.zhihu.com/question/41922036/answer/93079526) 和 一篇博文(http://m635674608.iteye.com/blog/2236137)。内容如下。

1. 知乎回答:https://www.zhihu.com/question/41922036/answer/93079526

針對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 GC。

最簡單的分代式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的工作量)。控制這個行為的VM參數是-XX:+ScavengeBeforeFullGC。這是HotSpot VM裡的奇葩嗯。可跳傳送門圍觀:JVM full GC的奇怪現象,求解惑? - RednaxelaFX 的回答

并發GC的觸發條件就不太一樣。以CMS GC為例,它主要是定時去檢查old gen的使用量,當使用量超過了觸發比例就會啟動一次CMS GC,對old gen做并發收集。

2. 博文:http://m635674608.iteye.com/blog/2236137

原文: Minor GC vs Major GC vs Full GC

在Plumbr進行GC暫停檢測功能的工作時, 我不得不閱讀大量與此相關的文章,書籍和報告。在研究過程中, 對于

Minor

Major

Full GC

時間我一再的困惑,這也就導緻本博文的産生, 希望我能理清我的一些困惑。

本文期望讀者熟悉JVM内建的垃圾回收的基本原理。JVM的記憶體堆對 

Eden

Survivor

 和 

Tenured/Old區

劃分, 代假設和不同的GC算法不在本文的讨論之列。

JVM 垃圾回收 Minor gc vs Major gc vs Full gc

Minor GC

在年輕代

Young space

(包括Eden區和Survivor區)中的垃圾回收稱之為 Minor GC. 這個定義既清晰又無異議。 但仍有一些有趣的關于Minor GC事件的東西你需要了解:

  1. Minor GC總是在不能為新的對象配置設定空間的時候觸發, 例如 Eden區滿了,配置設定空間的越快,Minor GC越頻繁。
  2. 當記憶體池慢了後, 它的完整的内容會被複制出去,指針可以從0開始重新跟蹤空閑記憶體。是以取代傳統的标記-交換-壓縮(Mark, Sweep , Compact), Eden區和Survivor區使用标記-複制方式(Mark , Copy). 是以在Eden區和Survivor區無記憶體碎片。寫指針總是指向記憶體池的頂部。
  3. 在Minor GC時, 年老代(Tenured generation)可以被忽略. 年老代對年輕代的引用被認為是實際的GC根root。 在标記階段年輕代對年老代的引用可以被簡單的忽略。
  4. 出于常理, 所有的Minor GC都會觸發stop-the-world暫停, 它意味着會暫停應用的所有線程. 對于大部分應用而言,暫停的延遲可以忽略不計。這是因為Eden中大部分的對象都可以垃圾回收掉,而不會被複制到Survivor/Old區。但如果相反,大部分的新對象不能被回收, Minor GC暫停會占用更多的時間。

綜上所述,

Minor GC

概念相當清晰 – 每次Minor GC隻會清理年輕代.

Major GC vs Full GC

有人可能會注意到沒有關于

Major GC

Full GC

正式的定義, 即使在JVM規範和垃圾回收論文中也沒有。但是輕輕一瞥,從我們對

Minor GC

定義上來看, 它們的定義也應該很簡單:

  • Major GC 清理年老區(Tenured space).
  • Full GC 清理整個記憶體堆 – 既包括年輕代也包括年老代.

不幸的是, 它有點複雜和令人不解. 首先來說,很多

Major GC

都是由

Minor GC

觸發的,是以很多情況下将這兩個概念分開是不可能的,另一方面,很多現代的垃圾回收會部分的執行年老代(Tenured space)清理,是以使用清理這個詞也隻能部分的正确。

這會引導我們了解到這一點: 與其擔心GC被稱作 Major 還是 Full GC, 你更應該關心GC是否會暫停程式的所有線程,還是和應用程式并行的處理.

這種困惑甚至内置于JVM的标準工具中. 最好通過例子來說明. 讓我們比較一下兩個GC跟蹤工具的輸出,此時JVM使用Concurrent Mark and Sweep collector (-XX:+UseConcMarkSweepGC)

首先看一下jstat 的輸出:

1 my-precious: me$ jstat -gc -t 4235 1s
1 2 3 4 5 6 7 8 9 10 11 12 13 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 00.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 00.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 00.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 70.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 00.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 00.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.8960 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.9780 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.0871 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.1832 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.3362 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.4332 0.050 1.484

這個片段摘自JVM啟動的前17秒。基于這些資訊我們可以得出結論, 經過12次Minor GC後運作了兩次Full GC,總共花費50ms (譯者按:檢視YGC和FGC數). 通過GUI工具你也應該能得到相同的資訊,比如 jconsole 或 jvisualvm.

在得出我們的結論之前,讓我們看一下同樣的JVM啟動時垃圾回收日志的輸出,顯然-XX:+PrintGCDetails可以告訴我們垃圾回收器工作的細節:

1 java -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC eu.plumbr.demo.GarbageProducer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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.0291598secs]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]

從上面的日志我們可以看到經過12次 Minor GC後一些“不同的東西”發生了。 不是兩次Full GC, 而是單一的在年老代(Tenured generation)的GC, 包括兩個階段:

  • 初始标記Mark階段, 大概花費0.0041705秒,約等于4毫秒. 這個階段是stop-the-world事件,會暫停所有應用的線程以便标記.
  • 并發執行Markup 和 Preclean階段. 它和應用程式的線程并發執行
  • 最終Remark階段, 花費0.0462010秒,大約46毫秒. 這個階段還是stop-the-world 事件.
  • 并發執行Sweep操作. 就像名字一樣,這個階段并發執行,不會暫停應用的線程.

就像我們從gc log中看到的,不是兩次Full GC操作,隻有一次Major GC用來清理年老區。

如果你遇到延遲的問題,然後基于jstat的結果做出決定, 這沒問題。它正确的列出了兩次stop-the-world事件的總耗時:50毫秒,它會導緻所有的應用線程的延遲。但是如果你想優化吞吐率,你可能被誤導了– 它隻列出了導緻stop-the-world的初始mark和最終remark階段,jstat輸出結果完全隐藏了Major GC并發工作。

結論

考慮到上面的情況,最好不要考慮Minor,Major和Full GC的術語, 相反,監控你的程式的延遲和吞吐率,以及和GC事件的關聯。檢查這些事件是否強制暫停應用程式的線程,或者事件是并發的執行。

堆記憶體劃分為 Eden、Survivor 和 Tenured/Old 空間,如下圖所示:

JVM 垃圾回收 Minor gc vs Major gc vs Full gc

從年輕代空間(包括 Eden 和 Survivor 區域)回收記憶體被稱為 Minor GC,對老年代GC稱為Major GC,而Full GC是對整個堆來說的,在最近幾個版本的JDK裡預設包括了對永生帶即方法區的回收(JDK8中無永生帶了),出現Full GC的時候經常伴随至少一次的Minor GC,但非絕對的。Major GC的速度一般會比Minor GC慢10倍以上。下邊看看有那種情況觸發JVM進行Full GC及應對政策。

1、System.gc()方法的調用

此方法的調用是建議JVM進行Full GC,雖然隻是建議而非一定,但很多情況下它會觸發 Full GC,進而增加Full GC的頻率,也即增加了間歇性停頓的次數。強烈影響系建議能不使用此方法就别使用,讓虛拟機自己去管理它的記憶體,可通過通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。

2、老年代代空間不足

老年代空間隻有在新生代對象轉入及建立為大對象、大數組時才會出現不足的現象,當執行Full GC後空間仍然不足,則抛出如下錯誤:

java.lang.OutOfMemoryError: Java heap space 

為避免以上兩種狀況引起的Full GC,調優時應盡量做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要建立過大的對象及數組。

3、永生區空間不足

JVM規範中運作時資料區域中的方法區,在HotSpot虛拟機中又被習慣稱為永生代或者永生區,Permanet Generation中存放的為一些class的資訊、常量、靜态變量等資料,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被占滿,在未配置為采用CMS GC的情況下也會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會抛出如下錯誤資訊:

java.lang.OutOfMemoryError: PermGen space 

為避免Perm Gen占滿造成Full GC現象,可采用的方法為增大Perm Gen空間或轉為使用CMS GC。

4、CMS GC時出現promotion failed和concurrent mode failure

對于采用CMS進行老年代GC的程式而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure兩種狀況,當這兩種狀況出現時可能

會觸發Full GC。

promotion failed是在進行Minor GC時,survivor space放不下、對象隻能放入老年代,而此時老年代也放不下造成的;concurrent mode failure是在

執行CMS GC的過程中同時有對象要放入老年代,而此時老年代空間不足造成的(有時候“空間不足”是CMS GC時目前的浮動垃圾過多導緻暫時性的空間不足觸發Full GC)。

對措施為:增大survivor space、老年代空間或調低觸發并發GC的比率,但在JDK 5.0+、6.0+的版本中有可能會由于JDK的bug29導緻CMS在remark完畢

後很久才觸發sweeping動作。對于這種狀況,可通過設定-XX: CMSMaxAbortablePrecleanTime=5(機關為ms)來避免。

5、統計得到的Minor GC晉升到舊生代的平均大小大于老年代的剩餘空間

這是一個較為複雜的觸發情況,Hotspot為了避免由于新生代對象晉升到舊生代導緻舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之

前統計所得到的Minor GC晉升到舊生代的平均大小大于舊生代的剩餘空間,那麼就直接觸發Full GC。

例如程式第一次觸發Minor GC後,有6MB的對象晉升到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大于6MB,如果小于6MB,

則執行Full GC。

當新生代采用PS GC時,方式稍有不同,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否

大于6MB,如小于,則觸發對舊生代的回收。

除了以上4種狀況外,對于使用RMI來進行RPC或管理的Sun JDK應用而言,預設情況下會一小時執行一次Full GC。可通過在啟動時通過- java -

Dsun.rmi.dgc.client.gcInterval=3600000來設定Full GC執行的間隔時間或通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。

6、堆中配置設定很大的對象

所謂大對象,是指需要大量連續記憶體空間的java對象,例如很長的數組,此種對象會直接進入老年代,而老年代雖然有很大的剩餘空間,但是無法找到足夠大的連續空間來配置設定給目前對象,此種情況就會觸發JVM進行Full GC。

為了解決這個問題,CMS垃圾收集器提供了一個可配置的參數,即-XX:+UseCMSCompactAtFullCollection開關參數,用于在“享受”完Full GC服務之後額外免費贈送一個碎片整理的過程,記憶體整理的過程無法并發的,空間碎片問題沒有了,但提頓時間不得不變長了,JVM設計者們還提供了另外一個參數 -XX:CMSFullGCsBeforeCompaction,這個參數用于設定在執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的。