Minor GC、Major GC以及Full GC的介紹及對比
在 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 輸出:
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的輸出看不到那些併發完成的工作。
結論
考慮到這種情況,最好避免以 Minor、Major、Full GC 這種方式來思考問題。而應該監控應用延遲或者吞吐量,然後將 GC 事件和結果聯絡起來。
隨著這些 GC 事件的發生,你需要額外的關注某些資訊,GC 事件是強制所有應用程式執行緒停止了還是並行的處理了部分事件。
如果你喜歡這篇我們垃圾回收手冊的示例篇,那麼請關注一下,整個教程將在2015年3月左右釋出。
相關文章
- jvm hotspot的minor major full gc之間的關係,以及哪些情況下會觸發full gcJVMHotSpotGC
- JVM 系列文章之 Full GC 和 Minor GCJVMGC
- 總結Minor GC、Full GC觸發條件GC
- JVM垃圾回收——新生代,老年代,永久代,Minor GC,Full GCJVMGC
- Full GC (Metadata GC Threshold)GC
- GC演算法介紹GC演算法
- 觸發JVM進行Full GC的情況及應對策略JVMGC
- Java GC專家系列4:Apache的MaxClients設定及其對Tomcat Full GC的影響JavaGCApacheclientTomcat
- 從CLR GC到CoreCLR GCGC
- JVM中記憶體和GC的介紹JVM記憶體GC
- CLR的GC工作模式介紹(Workstation和Server)GC模式Server
- GCGC
- [Inside HotSpot] Serial垃圾回收器 (二) Minor GCIDEHotSpotGC
- GC演算法介紹及工作原理和優缺點GC演算法
- GC.MaxGeneration屬性【GC示例】GC
- 從一次 FULL GC 卡頓談對服務的影響GC
- 頻繁GC (Allocation Failure)及young gc時間過長分析GCAI
- [Inside HotSpot] Serial垃圾回收器Full GCIDEHotSpotGC
- GC是什麼?為什麼要有GC?GC
- Android GC,JVM MinorGC/ MajorGC/ FullGC,GC目的AndroidGCJVM
- Java虛擬機器6:記憶體溢位和記憶體洩露、並行和併發、Minor GC和Full GC、Client模式和Server模式的區別Java虛擬機記憶體溢位記憶體洩露並行GCclient模式Server
- python的GCPythonGC
- 淺談對java-GC的理解JavaGC
- gc buffer busyGC
- gc 檢視GC
- System.gc()GC
- GC故障2GC
- GC故障3GC
- P於GCGC
- java GC CollectorJavaGC
- .NET GC 實時監控 dotnet-gcmon 介紹GC
- gc current block pin time gc current block flush time 疑惑GCBloC
- Java GC 專家系列3:GC調優實踐JavaGC
- 【GC】安裝GC之前需要調整的幾個引數GC
- JVM的GC日誌JVMGC
- 什麼是垃圾蒐集(GC)?為什麼要有GC呢?GC
- 對一次 GC日誌的分析GC
- Unity GC垃圾回收UnityGC