首页 > 学院 > 开发设计 > 正文

四、GC算法实现

2019-11-06 06:25:08
字体:
来源:转载
供稿:网友

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

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

新生代GC算法 老年代GC算法 JVM参数
Incremental Incremental -Xincgc
Serial Serial -XX:+UseSerialGC
Parallel Scavenge Serial -XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel New Serial N/A
Serial Parallel Old N/A
Parallel Scavenge Parallel Old -XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel New Parallel Old N/A
Serial CMS -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
Parallel Scavenge CMS N/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-02001 : 151.12622 : [ GC3 (Allocation Failure4 151.126: [DefNew5 : 629119K->69888K6 (629120K)7> , 0.0584157 secs] 1619346K-1273247K8 (2027264K)9, 0.0585007> secs10] [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-02001 : 172.8292 : [GC (Allocation Failure 172.829: [DefNew: 629120K->629120K(629120K), 0.0000372 secs3] 172.829:[Tenured4: 1203359K->755802K5 (1398144K)6, 0.1855567 secs7 ] 1832479K->755802K8 (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.MyExecutableClassjava -XX:+UseParallelOldGC com.mypackages.MyExecutableClassjava -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-02001: 116.1152: [GC3 (Allocation Failure4) [PSYoungGen5: 2694440K->1305132K6(2796544K)7] 9556775K->8438926K8(11185152K)9, 0.2406675 secs10] [Times: user=1.77 sys=0.01, real=0.24 secs]11

   (1)2015-05-26T14:27:40.915-0200,本次GC事件发生的时间 (2)116.115,GC时JVM的启动总时间,单位为秒。 (3)GC,用于区分Minor GC和Full GC。这里表示本次为Minor GC (4)Allocation Failure,导致本次GC的原因。是由于新生代中无法为新对象分配内存 (5)PSYoungGen,垃圾收集器的名称,这里表示这是一个并行标记复制,暂停全部应用的新生代垃圾收集器 (6)2694440K->1305132K,GC前后新生代的内存空间使用量 (7)2796544K,新生代总内存量 (8)9556775K->8438926K,垃圾回收前后总堆内存的变化情况(包括新生代和老年代) (9)11185152K,JVM堆的可用内存 (10)0.2406675 secs,GC事件的耗时 (11)[Times: user=1.77 sys=0.01, real=0.24 secs],GC事件的耗时,

user- 表示在此次垃圾回收过程中,所有GC线程消耗的CPU时间之和sys - 表示GC过程中操作系统调用和系统等等事件所消耗的时间

real - 应用暂停的总时间。在并行GC中,这个数值应该接近于(user + sys) / GC线程数。即单个核上平均的暂停时间,在这里线程数为8。由于某些过程是不能并行执行的,所以这个值会比刚才求的均值略高。

  总结一下本次GC过程就是,在GC前整个堆内存使用了9556775K,其中新生代使用了2694440K,那么老年代使用了6862335K。新生代的GC导致新生代释放了1389308K的空间,但是堆的总空间只释放了1117849K。这意味着有271459K的对象从新生代升级到了老年代中,整个过程如下图所示,      这里写图片描述

2、Full GC

  在理解了新生代的并行GC过程后,我们接下来分析一些并行GC在Full GC时的表现,

2015-05-26T14:27:41.155-02001: 116.3562: [Full GC3 (Ergonomics4) [PSYoungGen: 1305132K->0K(2796544K)]5 [ParOldGen6: 7133794K->6597672K7(8388608K)8] 8438926K->6597672K9(11185152K)10,[Metaspace: 6745K->6745K(1056768K)]11, 0.9158801 secs12] [Times: user=4.49 sys=0.64,real=0.92 secs]13

   (1)2015-05-26T14:27:41.155-0200,本次GC事件发生的时间 (2)116.356,GC时JVM的启动总时间,单位为秒。 (3)Full GC,表示本次是一次Full GC,将会对新生代和老年代的内存空间进行回收 (4)Ergonomics,本次GC的触发原因。这里是由于JVM认为此刻是一次适合进行垃圾回收的时间 (5)[PSYoungGen: 1305132K->0K(2796544K)],垃圾收集器的名称。PSYoungGen表示这是一次新生代中进行的标记复制,暂停全部应用的新生代GC。新生代的内存空间使用量从1305132K降低到0。一般来说,进行了一次Full GC后,新生代的内存空间将会被全部清理。 (6)ParOldGen,老年代中的垃圾收集器类型。在这里ParOldGen表示在老年代中使用的标记清除整理,暂停全部应用的老年代垃圾收集器。 (7)133794K->6597672K,老年代垃圾回收前后的内存使用情况 (8)8388608K,老年代的总内存大小 (9)8438926K->6597672K,垃圾回收前后总堆内存的变化情况(包括新生代和老年代) (10)11185152K,JVM堆的总内存 (11)[Metaspace: 6745K->6745K(1056768K)],元数据区在垃圾回收前后的内存使用情况,从这里可以看出,本次GC时并没有对元数据区的内存进行回收 (12)0.9158801 secs,本次GC的耗时,单位为秒 (13)[Times: user=4.49 sys=0.64,real=0.92 secs],GC事件的耗时,

user- 表示在此次垃圾回收过程中,所有GC线程消耗的CPU时间之和sys - 表示GC过程中操作系统调用和系统等等事件所消耗的时间real - 应用暂停的总时间。在并行GC中,这个数值应该接近于(user + sys) / GC线程数。即单个核上平均的暂停时间,在这里线程数为8。由于某些过程是不能并行执行的,所以这个值会比刚才求的均值略高。      并行GC过程中的Full GC也与Minor GC有些不同。Full GC不仅会对新生代进行垃圾回收,也会清理老年代和元数据区。在Full GC前后,JVM各区内存变化情况如下图所示,      这里写图片描述

三、并发标记清除CMS(Concurrent Mark and Sweep)

四、G1(Garbage First)

五、 Shenandoah


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表