Minor GC、Major GC、Full GC的區別)——JVM系列(八)

寫在前面:2020年面試必備的Java後端進階面試題總結了一份複習指南在Github上,內容詳細,圖文並茂,有需要學習的朋友可以Star一下!
GitHub地址:https://github.com/abel-max/Java-Study-Note/tree/master

在 Plumbr 從事 GC 暫停檢測相關功能的工作時,我被迫用自己的方式,通過大量文章、書籍和演講來介紹我所做的工作。在整個過程中,經常對 Minor、Major、和 Full GC 事件的使用感到困惑。這也是我寫這篇博客的原因,我希望能清楚地解釋這其中的一些疑惑。

文章要求讀者熟悉 JVM 內置的通用垃圾回收原則。堆內存劃分爲 Eden、Survivor 和 Tenured/Old 空間,代假設和其他不同的 GC 算法超出了本文討論的範圍。

Minor GC、Major GC和Full 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 輸出:
image.png

這個片段是 JVM 啓動後第17秒提取的。基於該信息,我們可以得出這樣的結果,運行了12次 Minor GC、2次 Full GC,時間總跨度爲50毫秒。通過 jconsole 或者 jvisualvm 這樣的基於GUI的工具你能得到同樣的結果。
image.png
在點頭同意這個結論之前,讓我們看看來自同一個 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 事件是強制所有應用程序線程停止了還是並行的處理了部分事件。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章