1、Java GC的工作原理

GC(garbage collection)是指垃圾回收机制,当一个对象不能再被后续程序所引用到时,这个对象所占用的内存空间就没有存在的意义了,java虚拟机会不定时的去检测内存中这样的对象,然后回收这块内存空间。当可用内存不能满足内存请求时,GC会自动进行。

所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xms(程序的初始化内存大小)和-Xmx(程序占用的最大内存)来控制。堆被划分为新生代和旧生代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace和ToSpace组成,结构图如下所示:

Full GC触发原理和日志分析_java

  • 新生代:
    新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中,新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例旧生代。用于存放新生代中经过多次垃圾回收仍然存活的对象。
    新生代的GC:
    新生代通常存活时间较短,因此基于Copying算法来进行回收,所谓Copying算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和FromSpace或ToSpace之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到survivor,最后到旧生代
  • 旧生代:
    旧生代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。
    在GC被触发的过程中,会导致服务暂停,因此关注FULLGC的总次数,计算服务暂停时间和频率,就是为了分析服务器的性能,从而根据具体情况进行性能优化。

2、触发Full GC执行的情况

一般Young区触发gc的条件是Eden区满。

除直接调用System.gc外,触发Full GC执行的情况有如下四种
1) 新生代对象转入及创建为大对象、大数组时导致旧生代空间出现不足的现象,为避免以上状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组

2) Permanet Generation中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
CMS:ConcurrentMarkSweep收集器,其目标是获取最短回收停顿时间,是server模式下最常用的收集器。

3) 对于采用CMS进行旧生代GC的程序,当GC日志中有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。应对措施:增大survivor space、旧生代空间或调低触发并发GC的比率。

4) 还有一个较为复杂的触发情况,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。

3、GC日志分析

发生GC时输出的日志就类似于下面这种格式(为了显示方便,已手工折行):

2015-05-26T14:45:37.987-0200: 151.126: 
  [GC (Allocation Failure) 151.126:
    [DefNew: 629119K->69888K(629120K), 0.0584157 secs]
    1619346K->1273247K(2027264K), 0.0585007 secs] 
  [Times: user=0.06 sys=0.00, real=0.06 secs]

2015-05-26T14:45:59.690-0200: 172.829: 
  [GC (Allocation Failure) 172.829: 
    [DefNew: 629120K->629120K(629120K), 0.0000372 secs]
    172.829: [Tenured: 1203359K->755802K(1398144K), 0.1855567 secs]
    1832479K->755802K(2027264K),
    [Metaspace: 6741K->6741K(1056768K)], 0.1856954 secs]
  [Times: user=0.18 sys=0.00, real=0.18 secs]
12345678910111213

上面的GC日志暴露了JVM中的一些信息。事实上,这个日志片段中发生了 2 次垃圾回收事件(Garbage Collection events)。其中一次清理的是年轻代(Young generation), 而第二次处理的是整个堆内存。下面我们来看,如何解读第一次GC事件,发生在年轻代中的小型GC(Minor GC):

2015-05-26T14:45:37.987-0200:151.126:

[GC(Allocation Failure)151.126: 

[DefNew:629119K->69888K(629120K), 0.0584157 secs]
1619346K->1273247K(2027264K),0.0585007 secs]
[Times: user=0.06 sys=0.00, real=0.06 secs]
1. 2015-05-26T14:45:37.987-0200 – GC事件(GC event)开始的时间点.
 2. 151.126 – GC事件的开始时间,相对于JVM的启动时间,单位是秒(Measured in seconds).
 3. GC – 用来区分(distinguish)是 Minor GC 还是 Full GC 的标志(Flag). 这里的 GC
    表明本次发生的是 Minor GC.
 4. Allocation Failure – 引起垃圾回收的原因.
    本次GC是因为年轻代中没有任何合适的区域能够存放需要分配的数据结构而触发的.
 5. DefNew – 使用的垃圾收集器的名字. DefNew 这个名字代表的是: 单线程(single-threaded),
    采用标记复制(mark-copy)算法的, 使整个JVM暂停运行(stop-the-world)的年轻代(Young
    generation) 垃圾收集器(garbage collector).
 6. 629119K->69888K – 在本次垃圾收集之前和之后的年轻代内存使用情况(Usage).
 7. (629120K) – 年轻代的总的大小(Total size).
 8. 1619346K->1273247K – 在本次垃圾收集之前和之后整个堆内存的使用情况(Total used heap).
 9. (2027264K) – 总的可用的堆内存(Total available heap).
 10. 0.0585007 secs – GC事件的持续时间(Duration),单位是秒.
 11. [Times: user=0.06 sys=0.00, real=0.06 secs] –
     GC事件的持续时间,通过多种分类来进行衡量: user – 此次垃圾回收, 垃圾收集线程消耗的所有CPU时间(Total CPU
     time). sys – 操作系统调用(OS call) 以及等待系统事件的时间(waiting for system event)
     real – 应用程序暂停的时间(Clock time). 由于串行垃圾收集器(Serial Garbage
     Collector)只会使用单个线程, 所以 real time 等于 user 以及 system time 的总和.

通过上面的分析, 我们可以计算出在垃圾收集期间, JVM 中的内存使用情况。在垃圾收集之前, 堆内存总的使用了 1.54G (1,619,346K)。其中, 年轻代使用了 614M(629,119k)。可以算出老年代使用的内存为: 967M(990,227K)。

下一组数据( -> 右边)中蕴含了更重要的结论, 年轻代的内存使用在垃圾回收后下降了 546M(559,231k), 但总的堆内存使用(total heap usage)只减少了 337M(346,099k). 通过这一点,我们可以计算出, 有 208M(213,132K) 的年轻代对象被提升到老年代(Old)中。

更多内容请关注微信公众号“外里科技

Full GC触发原理和日志分析_CMS_02