轉載:http://www.importnew.com/15820.html
區別
在 Plumbr 從事 GC 暫停檢測相關功能的工作時,我被迫用自己的方式,通過大量文章、書籍和演講來介紹我所做的工作。在整個過程中,經常對 Minor、Major、和 Full GC 事件的使用感到困惑。這也是我寫這篇博客的原因,我希望能清楚地解釋這其中的一些疑惑。
文章要求讀者熟悉 JVM 內置的通用垃圾回收原則。堆內存划分為 Eden、Survivor 和 Tenured/Old 空間,代假設和其他不同的 GC 算法超出了本文討論的范圍。
Minor GC
從年輕代空間(包括 Eden 和 Survivor 區域)回收內存被稱為 Minor GC。這一定義既清晰又易於理解。但是,當發生Minor GC事件的時候,有一些有趣的地方需要注意到:
- 當 JVM 無法為一個新的對象分配空間時會觸發 Minor GC,比如當 Eden 區滿了。所以分配率越高,越頻繁執行 Minor GC。
- 內存池被填滿的時候,其中的內容全部會被復制,指針會從0開始跟蹤空閑內存。Eden 和 Survivor 區進行了標記和復制操作,取代了經典的標記、掃描、壓縮、清理操作。所以 Eden 和 Survivor 區不存在內存碎片。寫指針總是停留在所使用內存池的頂部。
- 執行 Minor GC 操作時,不會影響到永久代。從永久代到年輕代的引用被當成 GC roots,從年輕代到永久代的引用在標記階段被直接忽略掉。
- 質疑常規的認知,所有的 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 輸出:
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 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的工具你能得到同樣的結果。
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.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的輸出看不到那些並發完成的工作。
結論
考慮到這種情況,最好避免以 Minor、Major、Full GC 這種方式來思考問題。而應該監控應用延遲或者吞吐量,然后將 GC 事件和結果聯系起來。
隨着這些 GC 事件的發生,你需要額外的關注某些信息,GC 事件是強制所有應用程序線程停止了還是並行的處理了部分事件。
Minor GC ,Full GC 觸發條件
Minor GC觸發條件
當Eden區滿時,新生代剩余內存空間放不下新對象,觸發Minor GC。
Full GC觸發條件
1. 調用System.gc時,系統建議執行Full GC,但是不必然執。
2. 老年代空間不足
老年代空間只有在新生代對象轉入及創建為大對象、大數組時才會出現不足的現象,當執行Full GC后空間仍然不足,則拋出如下錯誤:
java.lang.OutOfMemoryError: Java heap space
為避免以上兩種狀況引起的FullGC,調優時應盡量做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要創建過大的對象及數組。
3. 方法區空間不足(老年代、元空間)
存放的為一些class的信息等,當系統中要加載的類、反射的類和調用的方法較多時,方法區可能會被占滿,在未配置為采用CMS GC的情況下會執行Full GC。如果經過Full GC 仍然回收不了,那么JVM會拋出如下錯誤信息:
java.lang.OutOfMemoryError: PermGen space 元空間報錯啥的
為避免Perm Gen占滿造成Full GC現象,可采用的方法為增大Perm Gen空間或轉為使用CMS GC。
4. 由Eden區、From Space區向To Space區復制時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小。
這里有2種:CMS GC時出現promotion failed(擔保:s1s2--old)和concurrent mode failure
對於采用CMS進行舊生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure兩種狀況,當這兩種狀況出現時可能會觸發Full GC。
promotionfailed是在進行Minor GC時,survivor space放不下、對象只能放入舊生代,而此時舊生代也放不下造成的;concurrent mode failure是在執行CMS GC的過程中同時有對 象要放入舊生代,而此時舊生代空間不足造成的。
應對措施為:增大survivorspace、舊生代空間或調低觸發並發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。
例如程序第一次觸發MinorGC后,有6MB的對象晉升到舊生代,那么當下一次Minor GC發生時,首先檢查舊生代的剩余空間是否大於6MB,如果小於6MB,則執行Full GC。
當新生代采用PSGC時,方式稍有不同,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. jmap -histo(人為執行該命令)
空間分配擔保失敗(可能導致Full GC頻繁)
在發生Minor GC之前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代所有對象的總空間,
-
如果大於,則此次Minor GC是安全的
-
如果小於,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。
如果HandlePromotionFailure=true,那么會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的對象的平均大小,如果大於,則嘗試進行一次Minor GC,但這次Minor GC依然是有風險的;如果小於或者HandlePromotionFailure=false,則改為進行一次Full GC。
上面提到了Minor GC依然會有風險,是因為新生代采用復制收集算法,假如大量對象在Minor GC后仍然存活(最極端情況為內存回收后新生代中所有對象均存活),而Survivor空間是比較小的,這時就需要老年代進行分配擔保,把Survivor無法容納的對象放到老年代。老年代要進行空間分配擔保,前提是老年代得有足夠空間來容納這些對象,但一共有多少對象在內存回收后存活下來是不可預知的,因此只好取之前每次垃圾回收后晉升到老年代的對象大小的平均值作為參考。使用這個平均值與老年代剩余空間進行比較,來決定是否進行Full GC來讓老年代騰出更多空間。
取平均值仍然是一種概率性的事件,如果某次Minor GC后存活對象陡增,遠高於平均值的話,必然導致擔保失敗,如果出現了分配擔保失敗,就只能在失敗后重新發起一次Full GC。雖然存在發生這種情況的概率,但大部分時候都是能夠成功分配擔保的,這樣就避免了過於頻繁執行Full GC。