Minor GC、Major GC和Full GC之间的区别

原文链接: javacodegeeks 翻译: ImportNew.com 光光头去打酱油
译文链接: http://www.importnew.com/15820.html

文章要求读者熟悉 JVM 内置的通用垃圾回收原则。堆内存划分为 Eden、Survivor 和 Tenured/Old 空间,代假设和其他不同的 GC 算法超出了本文讨论的范围。

fd0c0db33776f042f62e5386131e487c

 

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

 

这个片段是 JVM 启动后第17秒提取的。基于该信息,我们可以得出这样的结果,运行了12次 Minor GC、2次 Full GC,时间总跨度为50毫秒。通过 jconsole 或者 jvisualvm 这样的基于GUI的工具你能得到同样的结果。

 

image

 

在点头同意这个结论之前,让我们看看来自同一个 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月左右发布。

 

来自知乎大牛的回答

作者:RednaxelaFX
链接: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做并发收集。

网友提问

image

 

作者:RednaxelaFX
链接:https://www.zhihu.com/question/48780091/answer/113063216
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

不奇怪,一切现象都是有原因的。

先来了解些背景信息。在题主所使用的JDK 6 update 27的HotSpot VM里,-XX:+UseParallelGC会启用题主所说的配置(这也是HotSpot VM在Server Class Machine上的默认配置)。

其中,负责执行minor GC(只收集young gen)的是PS Scavenge,全称是ParallelScavenge,是个并行的copying算法的收集器;
而负责执行full GC(收集整个GC堆,包括young gen、old gen、perm gen)的是PS MarkSweep——但整个收集器并不是并行的,而在骨子里是跟Serial Old是同一份代码,是串行收集的。其算法是经典的LISP2算法,是一种mark-compact GC(不要被MarkSweep的名字骗了)。
(注意这个跟使用-XX:+UseParallelOldGC所指定的并不是同一个收集器,那个是PS Compact,是个并行的全堆收集器)

ParallelScavenge这个GC套装的full GC有个很特别的实现细节,那就是:当触发full GC的时候,实际上会先使用PS Scavenge执行一次young GC收集young gen,然后紧接着去用PS MarkSweep执行一次真正的full GC收集全堆。
所以说题主看到的现象就是很正常的一次ParallelScavenge的full GC而已。

要控制这个行为,可以使用VM参数:

  1 product(bool, ScavengeBeforeFullGC, true,                                 \
  2         "Scavenge youngest generation before each full GC, "              \
  3         "used with UseParallelGC")
  4 

指定 -XX:-ScavengeBeforeFullGC 就可以不在执行full GC的时候先执行一次PS Scavenge。

jdk6/jdk6/hotspot: 7561dfbeeee5 src/share/vm/gc_implementation/parallelScavenge/psMarkSweep.cpp

  1 void PSMarkSweep::invoke(bool maximum_heap_compaction) {
  2     // ...
  3 
  4     if (ScavengeBeforeFullGC) {
  5         PSScavenge::invoke_no_policy();
  6     }
  7 
  8     // ...
  9 }
 10 

================================

题主说:

这次FullGC为什么会被触发?回收前Old Gen的使用率是194M/910M,Survivor只占用了300多K,Eden则是0,此时Old Gen空间很富余,从YoungGen晋升的对象也只有300多K,PermGen也很富余……按照我的理解,似乎此时不应该触发FullGC啊?

正因为上面说的,ParallelScavenge这套GC在触发full GC时实际上会先做一个young GC(PS Scavenge),再执行真正的full GC(PS MarkSweep),所以题主在看数据的时候就被弄晕了:
题主实际看到的是在“真正的full GC”的数据,而这是在刚刚做完那个young GC后的,所以自然,此时edgen是空的,而survivor space里的对象都是活的。

要看这次full GC为何触发,必须去看这个因为full GC而触发的young GC之前的状态才行。

================================

另外,做完full GC后old gen的使用量上升也是非常正常的行为。HotSpot的full GC实现中,默认young gen里所有活的对象都要晋升到old gen,实在晋升不了才会留在young gen。假如做full GC的时候,old gen里的对象几乎没有死掉的,而young gen又要晋升活对象上来,那么full GC结束后old gen的使用量自然就上升了。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值