四GC算法实现

Posted dabokele

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了四GC算法实现相关的知识,希望对你有一定的参考价值。

  在了解了上一章中GC算法的基本概念之后,本章将深入到各GC算法的具体实现中。对大多数JVM来说,一般需要选择两种GC算法,一种用于回收新生代内存区,另一种用于回收老年代内存区域。

  新生代和老年代GC算法的可能组合如下表所示,如果不指定的话,将会在新生代和老年代中选择默认的GC算法。下表中的GC算法组合是基于Java 8的,在其他Java版本中可能会有所不同。

新生代GC算法老年代GC算法JVM参数
IncrementalIncremental-Xincgc
Serial Serial -XX:+UseSerialGC
Parallel ScavengeSerial-XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel NewSerialN/A
SerialParallel OldN/A
Parallel Scavenge Parallel Old -XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel NewParallel OldN/A
SerialCMS-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
Parallel ScavengeCMSN/A
Parallel New CMS -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
G1 -XX:+UseG1GC

  
  如果现在觉得上表看起来觉得很复杂,请别着急。一般常用的是上面加粗的四种组合。剩下的组合一般是已经不用了,或者是不再支持,或者在实际中基本不使用。所以,在接下来的文章中,只介绍上面这四种组合。

  • 新生代和老年代的串行GC(Serial GC)
  • 新生代和老年代的并行GC(Parallel GC)
  • 新生代并行GC(Parallel GC) + 老年代CMS
  • 部分新生代老年代的G1

一、串行GC(Serial GC)

  串行GC对于新生代使用标记复制(mark-copy)策略,对老年代使用标记清除整理(mark-sweep-compact)策略进行垃圾回收。这些收集器是单线程的,不能并发的对垃圾进行回收。并且在垃圾回收动作时会暂停整个应用线程(stop-the-world)。

  这种GC算法无法充分利用硬件资源,即使有多个核,在GC时也只用其中一个。在新生代和老年代启动串行GC的命令如下:

java -XX:+UseSerialGC com.mypackages.MyExecutableClass

  这种GC算法一般并不常用,只有在堆内存为几百MB,并且应用运行在单核CPU上时才使用。一般应用都部署在多核的服务器上,如果使用串行GC会在GC时无法充分利用资源,造成性能瓶颈,提高应用延迟和降低吞吐量。

  接下来我们看一个串行GC的垃圾收集日志信息,使用如下命令使应用打印出GC日志,

-XX:+PringGCDetails -XX:+PringGCDateStamps -XX:+PringGCTimeStamps

  输出日志如下,

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]

  从上面这段日志信息中可以看到进行了两次GC,第一次清理了新生代,第二次清理了新生代和老年代空间。

1、Minor GC

  清理新生代内存的GC事件日志如下,

2015-05-26T14:45:37.987-0200 1 : 151.1262 2 : [ GC 3 (Allocation Failure 4 151.126: [DefNew 5 : 629119K->69888K 6 (629120K) 7 > , 0.0584157 secs] 1619346K-1273247K 8 (2027264K) 9 , 0.0585007> secs 10 ] [Times: user=0.06 sys=0.00, real=0.06 secs] 11

  
  对照上面的不同字段进行说明,
(1)2015-05-26T14:45:37.987-0200,发生本次GC动作的时间
(2)151.126,GC事件发生时距离该JVM启动的时间,单位为秒
(3)GC,用于区分是Minor GC还是Full GC。这里表示本次是Minor GC
(4)Allocation Failure,导致本次进行GC的原因。在这里,本次GC是由于无法为新的数据结构在新生代中分配内存空间导致的。
(5)DefNew,垃圾收集器的名称。这个名称表示的是在新生代中进行的单线程,标记-复制,全应用暂停的垃圾收集器
(6)629119K->69888K,表示新生代内存空间在GC前后的大小。
(7)629120K,表示新生代的总大小
(8)1619346K->1273247K,堆内存在GC前后的大小
(9)2027264K,堆内存中可用大小
(10)0.0585007 secs,GC动作的时间,单位为秒
(11)Times: user=0.06 sys=0.00, real=0.06 secs,GC动作的时间,其中

  • user- 表示在此次垃圾回收过程中,所有GC线程消耗的CPU时间之和
  • sys - 表示GC过程中操作系统调用和系统等等事件所消耗的时间
  • real - 应用暂停的总时间。由于串行GC是单线程的,所以暂停总时间等于user时间和sys时间之和
      
      经过上面这些分析后,我们可以更加清楚的从GC日志中获取到当时的详细信息。在GC前,总共使用了1619346K堆内存,其中新生代使用了629119K。通过计算就可以得到老年代使用了990227K。

      GC后,新生代释放出了559231K内存空间,但是堆的总内存仅仅释放了346099K。也就是说,在本次GC时,有213132K的对象从新生代升级到了老年代区域。

      下图形象的表明了本次GC前后内存的变化情况。
      
      

2、Full GC

  理解了Minor GC事件后,接下来我们看一下第二次GC的日志,

2015-05-26T14:45:59.690-0200 1 : 172.829 2 : [GC (Allocation Failure 172.829: [DefNew: 629120K->629120K(629120K), 0.0000372 secs 3 ] 172.829:[Tenured 4 : 1203359K->755802K 5 (1398144K) 6 , 0.1855567 secs 7 ] 1832479K->755802K 8 (2027264K) 9 , [Metaspace: 6741K->6741K(1056768K)] 10 [Times: user=0.18 sys=0.00, real=0.18 secs] 11

  
  对上面各组数据进行分析,
(1)2015-05-26T14:45:59.690-0200,本次GC事件发生的时间
(2)172.829,GC时JVM的启动总时间,单位为秒。
(3)[DefNew: 629120K->629120K(629120K), 0.0000372 secs,由于分配内存不足导致的一次新生代GC。在本次GC时,首先进行的是新生代的DefNew类型GC,将新生代的内存使用从629120K降低到0。注意在这里,JVM的显示有问题,误认为年轻代内存使用完了。本次GC耗时0.0000372秒
(4)Tenured,老年代垃圾收集器的名称。Tenured表示一个单线程,暂停整个应用线程的标记清除整理的垃圾收集过程。
(5)1203359K->755802K,老年代在垃圾回收前后的内存使用情况
(6)1398144K,老年代总内存数
(7)0.1855567 secs,老年代垃圾回收的耗时
(8)1832479K->755802K,垃圾回收前后总堆内存的变化情况(包括新生代和老年代)
(9)2027264K,JVM堆的可用内存
(10)[Metaspace: 6741K->6741K(1056768K)],元数据区在垃圾回收前后的内存使用情况,从这里可以看出,本次GC时并没有对元数据区的内存进行回收
(11)[Times: user=0.18 sys=0.00, real=0.18 secs],GC事件的耗时,

  • user- 表示在此次垃圾回收过程中,所有GC线程消耗的CPU时间之和
  • sys - 表示GC过程中操作系统调用和系统等等事件所消耗的时间
  • real - 应用暂停的总时间。由于串行GC是单线程的,所以暂停总时间等于user时间和sys时间之和
      
      本次Full GC与上面的Minor GC区别十分明显,Full GC是会对老年代和元数据区进行垃圾回收的。本次垃圾回收的过程如下图所示,
      
      
      

二、并行GC(Parallel GC)

  在这种GC模式下,新生代使用标记复制策略,老年代使用标记清除整理策略。新生代和老年代的GC事件都会导致所有应用线程暂停。新生代和老年代在复制(copy)或整理(compact)阶段都使用多线程,这也是并行GC名称的来由。使用这种GC算法,可以降低垃圾回收的时间消耗。
  
  在垃圾回收时的并行线程数,可以由参数-XX:+ParallelGCThreads=NNN来设置。该参数的默认值是服务器的核数。
  
  使用并行GC,可以用以下三种命令模式:

java -XX:+UseParallelGC com.mypackages.MyExecutableClass
java -XX:+UseParallelOldGC com.mypackages.MyExecutableClass
java -XX:+UseParallelGC -XX:+UseParallelOldGC com.mypackages.MyExecutableClass

  
  并行垃圾收集器一般用在多核服务器上,在多核服务器上使用并行GC,能重复利用硬件资源,提高应用的吞吐量,
- 在垃圾收集过程中,会利用所有的核并行进行垃圾回收动作,降低应用暂停时间
- 在垃圾回收间歇期,垃圾收集器不工作,不会消耗系统资源
  
  另一方面,并行GC的所有阶段都不能被中断,所以这些垃圾收集器仍然有可能在所有应用线程停止时陷入长时间的暂停中。所以,如果要求系统低延迟,那么不建议使用这种垃圾收集器。
  
  接下来,我们看一下并行GC时的日志信息。如下所示,

2015-05-26T14:27:40.915-0200: 116.115: [GC (Allocation Failure) [PSYoungGen: 2694440K->1305132K(2796544K)] 9556775K->8438926K(11185152K), 0.2406675 secs] [Times: user=1.77sys=0.01, real=0.24 secs]

2015-05-26T14:27:41.155-0200: 116.356: [Full GC (Ergonomics) [PSYoungGen: 1305132K->0K(2796544K)] [ParOldGen: 7133794K->6597672K(8388608K)] 8438926K->6597672K(11185152K),[Metaspace: 6745K->6745K(1056768K)], 0.9158801 secs] [Times: user=4.49 sys=0.64,real=0.92 secs]

1、Minor GC

  接下来详细分析Minor GC时的日志信息。

2015-05-26T14:27:40.915-0200 1 : 116.115 2 : [GC 3 (Allocation Failure 4 ) [PSYoungGen 5 : 2694440K->1305132K 6 (2796544K) 7 ] 9556775K->8438926K 8 (11185152K) 9 , 0.2406675 secs 10 ] [Times: user=1.77 sys=0.01, real=0.24 secs] 11

  
(1)201

以上是关于四GC算法实现的主要内容,如果未能解决你的问题,请参考以下文章

如果由于CMS gc算法中的关键内存碎片而无法分配内存会发生什么

Young GC和Full GC分别在什么情况下会发生?

大厂面试题:Young GC和Full GC分别在什么情况下会发生?

深入JVM《四》 GC算法与种类

GC垃圾回收(3)- 三色标记算法

JAVA秒会技术之玩搞定GCGC算法与种类