vlambda博客
学习文章列表

Parnew 和CMS 垃圾回收器

前言

这篇文章主要介绍Parnew 和CMS 组合的新老年代组合垃圾回收,以及一些机制、问题等。

Parnew 垃圾回收器

1. 概述

ParNew 收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之 外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规 则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。

2. 特点

Parnew 垃圾回收器主打的就是对新生代多线程垃圾回收机制;另外一种Serial 垃圾回收器主打的是单线程垃圾回收,两者都是新生代回收,唯一的区别就是单线程和多线程,但是回收算法都是复制算法。在工作时会把系统程序的工作线程全部停止,禁止程序继续创建新的对象,然后就用多个垃圾回收线程进行垃圾回收,线程数量 和CPU 核心数一致,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。。

3. 使用

通过配置-XX: +UseParnewGC 选项,JVM启动之后对新生代进行垃圾回收就是ParNew 垃圾回收器了。

4. ParNew 检查机制

  1. 先判断新生代中所有对象大小  是否小于 老年代的可用区域 如果为 true 则触发ygc,  false 则进行下面2判断;
  2. 如果设置了-XX: HandlePromotionFailure这个参数,那么进入下面3判断,如果没设置则进行full gc;
  3. 判断ygc 历次进入老年代的平均大小 是否小于老年代可用空间大小 如果为 true 则触发ygc;如果为 false, 则触发full gc.

5. 回收过程

新生代分为三个区域:eden,survivor from,survivor to。

  1. 触发时机: 当Eden 和其中一个Survivor 区空间不足时,便会触发minor gc;
  2. 首先标记出存活的对象;
  3. 复制存活对象到survivor to;
  4. 清除Eden,survivor from 空间;

保证了空间利用率达到90%以上。

6. 进入老龄代情况

  1. 超过指定年龄(默认15)
    老龄代年龄可设置最大只能是15 ,见jdk源码makeOpp.hpp 对象头年龄4位;

  2. 大对象

  3. 动态年龄判断
    survivor 中已经存在了对象,此时放入一批对象刚好大于survivor 的50%。那么将大于50% 这些的年龄代的放入到老年代。

  4. 空间分配担保 在开启了-XX:HandlePromotionFailure情况下,年轻代在回收时,根据自己历代回收垃圾平大小和老年代可用空间大小做比较。

7. Minor GC 过后可能出现情况

  1. ygc 前先判断, 过后存活对象大小  < survivor 区域空间大小,则直接进入survivor;
  2. ygc 前先判断,老年代可用空间大小  > 过后存活对象大小  > survivor 区域空间大小, 则直接进入老年代;
  3. ygc 前先判断,过后存活对象大小  > survivor 区域空间大小 && 老年代可用空间大小  < 过后存活对象大小 ;

CMS(Concurrent Mark Sweep) 垃圾回收器

1. 概述

小内存(8G以下)老年代垃圾回收器一般选择CMS,采用的是标记-清理算法,先是标记方法(GC Roots 可达性)去标记出哪些对象是垃圾对象,然后就把这些垃圾对象一次性清理掉。这种方法最大的问题是造成很多内存碎片,CMS垃圾回收器采取的是垃圾回收线程和系统工作线程尽量同时执行的模式来处理。

2. CMS 执行一次垃圾回收过程

CMS 垃圾回收分为四个阶段:初始标记, 并发标记, 重新标记, 并发清理

  1. 初始标记阶段
    根据GC Roots 可达性标记出那些对象存在引用、以及垃圾对象;初始标记会stw 暂停一切工作线程,但是影响不大,因为他的速度很快,仅仅标记GC Roots 直接引用的那些对象罢了。

  2. 并发标记阶段
    这个阶段会和系统线程并行运行,可以继续创建新的对象,也可能让部分存活对象失去引用,变成垃圾对象,这个过程尽可能对已有的对象进行GC Roots 追踪,是最耗时的操作。

  3. 重新标记阶段
    这个阶段会继续让系统线程停下来stw,然后重新标记在第二阶段里新创建的对象,还有一些已有对象失去引用变成垃圾对象的情况,此阶段速度很快。

  4. 并发清理阶段
    这个阶段会和系统程序并行运行,然后垃圾对象从内存上的各种随机位置清除,并行运行所以不影响系统程序运行,随机清除耗时久。

3. 触发时机

老年代在内存达到一定比例,就自动执行GC。可以通过-XX: CMSInitiatingOccupancyFaction 参数设定老年代占用多少比例是会触发CMS 垃圾回收。JDK1.6 是92%,剩下的8% 就是留给并发回收垃圾期间,系统程序把一些新对象放入老年代中的。

4. CMS 垃圾回收器缺点

  1. 并发回收垃圾导致CPU 资源紧张

并发标记、并发清理 两个最耗时阶段和系统线程并发运行,会导致CPU 资源被垃圾回收线程占用一部分。CMS 默认启动垃圾回收线程:(CPU 核数+3)/4。

  1. Concurrent Mode Failure 问题
    浮动垃圾:并发清理期间,系统程序可能先把某些对象分配在新生代,然后可能触发了一次Minor GC,一些对象进入了老年代,然后这些对象又没了引用。浮动垃圾只能等待下次回收。所以CMS 在垃圾回收期间会预留一些空间,这个空间的大小就是-XX: CMSInitiatingOccupancyFaction 参数控制。如果这个时候系统程序执行mainor GC 新产生的老年代对象大小大于 预留空间空间大小就会发生Concurrent Mode Failure 问题,这个时候并发垃圾回收失败,会自动启用Serial Old 垃圾回收器替代CMS,就是直接强行把系统程序stw,重新进行长时间的GC Roots追踪,标记出来全部垃圾对象,不允许产生新的对象。

  2. 内存碎片问题
    CMS 使用的是标记-清理算法,在第4阶段并发清理时,会根据标记的垃圾对象从内存中随机位置清除,那么就会存在内存碎片,Full GC 越多,内存碎片越多,Full GC就越频繁。CMS有一个参数-XX:UseCMSCompactAtFullCollention(默认开启)控制每次Full GC之后就再次进入到stw ,停止系统线程工作,然后进行内存碎片整理,将存活对象移到一起,空出连续的内存空间,还有一个参数-XX:CMSFullGCsBeforeCompaction 标识执行多少次后进行一次内存碎片整理,默认0次。

  3. CMS 回收比年轻代回收慢10倍+?

    1. 新生代垃圾回收只需要从GC Roots 触发标记出那些对象是活的就行,新生代存活的对象很少,因此速度很快,不需要追踪多少对象。垃圾回收时,只需要把存活对象放到survivor to 区域即可,然后一次性直接回收Eden 和survivor from 即可。
    2. CMS 的full gc 要经过并发标记阶段,老年代存活对象很多,持续追踪的对象也就多,更耗时;并发清理阶段,不是一次性回收大片内存,而是零零星星的找到各个地方的垃圾对象,然后回收;full gc 之后还有内存碎片整理,经历 stw 将存活对象移到一起,空出连续内存空间。万一并发清理期间,出现了 Concurrent Mode Failure问题,就会启用 Serial Old垃圾回收器, stw 之后重新清理一遍。
  4. 触发老年代Full GC 情况?

    1. 老年代可用内存小于新生代全部对象的大小,如果没开启空间担保机制,就会触发Full GC,所以一般空间担保机制都会开启;
    2. 老年代可用内存小于历次新生代进入老年代平均大小,此时会提前Full GC;
    3. 新生代Minor GC 后存活的对象大于Survivor区域大小,那么就会进入老年代。

由于篇幅有限,感兴趣的看官可以自行翻阅资料;如有不妥之处,还望看官指正,谢谢大家。