`
m635674608
  • 浏览: 4905190 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

JVM 垃圾回收 Minor gc vs Major gc vs Full gc

 
阅读更多

原文: Minor GC vs Major GC vs Full GC

在Plumbr进行GC暂停检测功能的工作时, 我不得不阅读大量与此相关的文章,书籍和报告。在研究过程中, 对于MinorMajorFull GC时间我一再的困惑,这也就导致本博文的产生, 希望我能理清我的一些困惑。
本文期望读者熟悉JVM内建的垃圾回收的基本原理。JVM的内存堆对 EdenSurvivor 和 Tenured/Old区划分, 代假设和不同的GC算法不在本文的讨论之列。


Minor GC

在年轻代Young space(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC. 这个定义既清晰又无异议。 但仍有一些有趣的关于Minor GC事件的东西你需要了解:

  1. Minor GC总是在不能为新的对象分配空间的时候触发, 例如 Eden区满了,分配空间的越快,Minor GC越频繁。
  2. 当内存池慢了后, 它的完整的内容会被复制出去,指针可以从0开始重新跟踪空闲内存。所以取代传统的标记-交换-压缩(Mark, Sweep , Compact), Eden区和Survivor区使用标记-复制方式(Mark , Copy). 因此在Eden区和Survivor区无内存碎片。写指针总是指向内存池的顶部。
  3. 在Minor GC时, 年老代(Tenured generation)可以被忽略. 年老代对年轻代的引用被认为是实际的GC根root。 在标记阶段年轻代对年老代的引用可以被简单的忽略。
  4. 出于常理, 所有的Minor GC都会触发stop-the-world暂停, 它意味着会暂停应用的所有线程. 对于大部分应用而言,暂停的延迟可以忽略不计。这是因为Eden中大部分的对象都可以垃圾回收掉,而不会被复制到Survivor/Old区。但如果相反,大部分的新对象不能被回收, Minor GC暂停会占用更多的时间。

综上所述,Minor GC概念相当清晰 – 每次Minor GC只会清理年轻代.

Major GC vs Full GC

有人可能会注意到没有关于Major GCFull 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 的输出:

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后运行了两次Full GC,总共花费50ms (译者按:查看YGC和FGC数). 通过GUI工具你也应该能得到相同的信息,比如 jconsole 或 jvisualvm.

在得出我们的结论之前,让我们看一下同样的JVM启动时垃圾回收日志的输出,显然-XX:+PrintGCDetails可以告诉我们垃圾回收器工作的细节:

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]

从上面的日志我们可以看到经过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事件的关联。检查这些事件是否强制暂停应用程序的线程,或者事件是并发的执行。

 

堆内存划分为 Eden、Survivor 和 Tenured/Old 空间,如下图所示:

从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,对老年代GC称为Major GC,而Full GC是对整个堆来说的,在最近几个版本的JDK里默认包括了对永生带即方法区的回收(JDK8中无永生带了),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。Major GC的速度一般会比Minor GC慢10倍以上。下边看看有那种情况触发JVM进行Full GC及应对策略。

 

1、System.gc()方法的调用

 

此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。


2、老年代代空间不足


老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space 
为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

3、永生区空间不足


JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space 
为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。


4、CMS GC时出现promotion failed和concurrent mode failure


对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能

会触发Full GC。
promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure是在

执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。
对措施为:增大survivor space、老年代空间或调低触发并发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。
例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,

则执行Full GC。
当新生代采用PS GC时,方式稍有不同,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、堆中分配很大的对象

 

所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。

为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。

 

http://blog.csdn.net/chenleixing/article/details/46706039

http://www.importnew.com/14086.html

http://www.importnew.com/15820.html

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics