likes
comments
collection
share

JVM-传统垃圾收集器

作者站长头像
站长
· 阅读数 9

JVM-传统垃圾收集器

  • 新生代收集器:
    • Serial
    • ParNew
    • Parallel Scavenge
  • 老年代收集器:
    • Serial Old
    • Parallel Old
    • CMS

一、新生代收集器

需要先说明,新生代的三个收集器,都是采用的 标记-复制算法,且内存布局把新生代划分为:Eden,From Survivor,To Survivor三块空间。

1.1 Serial收集器

这个收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。

下图,红色虚线框住的部分,就是 Serial收集器运行情况: JVM-传统垃圾收集器 迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,它是所有收集器里额外内存消耗(Memory Footprint)最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

1.2 ParNew收集器

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

ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程(Hyper-Threading)技术实现的伪双核处理器环境中都不能百分之百保证超越Serial收集器。当然,随着可以被使用的处理器核心数量的增加,ParNew对于垃圾收集时系统资源的高效利用还是很有好处的。

下图,红色虚线框住的部分,就是 ParNew收集器运行情况:(注意:GC线程是多线程的)

JVM-传统垃圾收集器

1.3 Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput) 。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,即:

JVM-传统垃圾收集器

如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数

  • -XX:MaxGCPauseMillis参数:

    允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。不过大家不要异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些,收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。

  • -XX:GCTimeRatio参数:

    该参数值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。

Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy:

这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)。如果读者对于收集器运作不太了解,手工优化存在困难的话,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成也许是一个很不错的选择。只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用-XX:MaxGCPauseMillis参数(更关注最大停顿时间)或-XX:GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性。

二、老年代收集器

2.1 Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。

下图,红色虚线框住的部分,就是 Serial Old收集器运行情况:(单线程,标记-整理,暂停所有用户线程) JVM-传统垃圾收集器

2.2 Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。

下图,红色虚线框住的部分,就是 Serial Old收集器运行情况:(多个GC线程,标记-整理,暂停所有用户线程) JVM-传统垃圾收集器

2.3 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:

  • 1)初始标记(CMS initial mark)

    仅仅只是标记一下GCRoots能直接关联到的对象,速度很快。

  • 2)并发标记(CMS concurrent mark)

    就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。

  • 3)重新标记(CMS remark)

    为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。

  • 4)并发清除(CMS concurrent sweep)

    清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

  • 其实还有一个第五阶段,就是下图中最后标记的 ,重置线程

    CMS通过重置阶段,等待下一次达到GC阈值。

如下图,是整个CMS收集器的运作过程,其中初始标记、重新标记这两个步骤仍然需要“Stop The World”;而并发标记和并发清理阶段,GC线程可以与用户线程一起工作。 JVM-传统垃圾收集器

  • 优点:

    • 并发收集、低停顿。
  • 缺点:

    • CMS收集器无法处理“浮动垃圾”(Floating Garbage) ,有可能出现“Con-current ModeFailure”失败进而导致另一次完全“Stop The World”的Full GC的产生。在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。
    • 由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。
    • 到了JDK 6时,CMS收集器的启动阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。
    • CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。所以,空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMS-CompactAtFullCollection开关参数(默认是开启的,此参数从JDK 9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的。这样空间碎片问题是解决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction(此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)。

三、垃圾收集器配合使用方式

JVM-传统垃圾收集器 如上图所示:

  • 组合一:

    • ① 新生代Serial收集器 和 老年代 Serial Old收集器配合使用;
    • ② 新生代Serial收集器,JDK 9 之前可以和 老年代CMS收集器配合使用,并且CMS在产生过多空间碎片的时候,可以利用 Serial Old 对老年代进行垃收集。(也就是说,JDK 9 开始这套组合不中了。)
  • 组合二:

    • ① 新生代ParNew收集器 在JDK 9之前可以和老年代Serial Old收集器配合使用;(JDK 9 开始这套组合不中了)
    • ② 新生代ParNew收集器,JDK 9 之前可以和 老年代CMS收集器配合使用,并且CMS在产生过多空间碎片的时候,可以利用 Serial Old 对老年代进行垃收集。(这套是 CMS 现在可以用的!!!)
  • 组合三:

    • ① 新生代Parallel Scavenge 可以和 老年代 Serial Old收集器 配合使用;
    • ② 新生代Parallel Scavenge 可以和 老年代 Parallel Old收集器 配合使用。(Parallel Scavenge 推荐这套组合拳)
  • G1:

    • 以 Region 划分内存区域,每个Region都可以是 Eden,Survivor,老年代,所以G1可以回收整个Java堆和方法区。