原文: Minor GC vs Major GC vs Full GC
在Plumbr进行GC暂停检测功能的工作时, 我不得不阅读大量与此相关的文章,书籍和报告。在研究过程中, 对于Minor
, Major
和Full GC
时间我一再的困惑,这也就导致本博文的产生, 希望我能理清我的一些困惑。
本文期望读者熟悉JVM内建的垃圾回收的基本原理。JVM的内存堆对 Eden
, Survivor
和 Tenured/Old区
划分, 代假设和不同的GC算法不在本文的讨论之列。
Minor GC
在年轻代Young space
(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC. 这个定义既清晰又无异议。 但仍有一些有趣的关于Minor GC事件的东西你需要了解:
- Minor GC总是在不能为新的对象分配空间的时候触发, 例如 Eden区满了,分配空间的越快,Minor GC越频繁。
- 当内存池慢了后, 它的完整的内容会被复制出去,指针可以从0开始重新跟踪空闲内存。所以取代传统的标记-交换-压缩(Mark, Sweep , Compact), Eden区和Survivor区使用标记-复制方式(Mark , Copy). 因此在Eden区和Survivor区无内存碎片。写指针总是指向内存池的顶部。
- 在Minor GC时, 年老代(Tenured generation)可以被忽略. 年老代对年轻代的引用被认为是实际的GC根root。 在标记阶段年轻代对年老代的引用可以被简单的忽略。
- 出于常理, 所有的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 的输出:
|
|
|
|
这个片段摘自JVM启动的前17秒。基于这些信息我们可以得出结论, 经过12次Minor GC后运行了两次Full GC,总共花费50ms (译者按:查看YGC和FGC数). 通过GUI工具你也应该能得到相同的信息,比如 jconsole 或 jvisualvm.
在得出我们的结论之前,让我们看一下同样的JVM启动时垃圾回收日志的输出,显然-XX:+PrintGCDetails可以告诉我们垃圾回收器工作的细节:
|
|
|
|
从上面的日志我们可以看到经过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事件的关联。检查这些事件是否强制暂停应用程序的线程,或者事件是并发的执行。
本文是我们的垃圾回收手册的一个例子章节。完整的手册大概在2015年三月发布 (译者按:目前还未找到,应该还未发布)。