垃圾收集器

2017-12-03  本文已影响489人  miaoLoveCode

注:一直都想要尝试介绍一下JVM的垃圾收集器,但是不知道从何开始,纠结了好久,还是尝试总结一波。本文将介绍JVM发展至今所有的垃圾收器。

前言

如果说垃圾收集器是内存回收的具体实现,那么内存收集算法就是内存回收方法论。在开始具体实现相关介绍之前,我们先来看一下内存收集算法。

注:由于垃圾收集算法底层实现细节较多,本文不分析hotspot具体实现,具体源码分析会在后续的博客陆续给出,请大家持续关注。

啰嗦完垃圾收集算法之后,我们接下来看看垃圾收集算法的具体实现--垃圾收集器的相关内容。

垃圾收集器

JVM的规范并没有规定垃圾收集器的实现方式,所以不同版本的JVM提供的垃圾收集器可能都是有差别的,当然,JVM也会提供参数以供用户自己选择和组合垃圾收集器。本文将会介绍6种作用于不用分区的收集器,它们几乎涵盖了JDK发展至今所有的主流垃圾收集器。

Serial收集器

Serial收集器应该是最基础的一款垃圾收集器,在JDK 1.3之前是JVM young区垃圾收集的唯一选择。Serial收集器是单线程的串行进行垃圾收集的收集器,而且,它在进行垃圾收集是必须要暂停所有的工作线程(STW),直到垃圾收集结束。

注:STW(Stop The World),由JVM自动发起和自动完成,在用户不知情的情况下把所有用户线程全部停掉。这样会导致很不好的用户体验,但是JVM的开发者们其实也很无奈,如果不暂停所有的用户线程,在边收集垃圾的过程中还不断有新的垃圾产生,循环往复,垃圾还能收集完么?

从JDK 1.3开始,JVM的开发锅锅们一直都在努力为消除/减少因内存回收而导致的STW,从Serial到Parallel再到CMS,G1,STW的时间在不断缩短,但是,STW还是仍然存在,并没有被完全消除。

看到这里,大家可能会觉得那Serial收集器是不是完全没用,但是其实到现在,它还是JVM运行在client模式下默认young区垃圾收集器。对于其他垃圾收集器,特别是单个CPU的时候,它有一个很明显的优点:简单高效,Serial收集器由于没有线程切换的相关开销,它只需要关注于垃圾收集,很自然也就可以获得最高的单线程收集效率。

同样,JVM提供以下参数控制垃圾收集:

ParNew收集器

ParNew收集器是Serial收集器的多线程版本,它和Serial收集器的唯一区别就是一个是多线程,一个是单线程,在hotspot的具体实现中,它们也共用了很多代码。

虽然ParNew比起Serial收集器并没有多少创新,但是它确实很多服务首选的young区收集器,其中有一个很重要的原因就是除Serial收集器之外,只有它能与CMS收集器配合使用。当然,JVM提供参数-XX:+UseParNewGC来设置选择ParNew收集器。

ParNew收集器在单CPU的环境中的效果其实没有Serial垃圾收集器好,但是,随着CPU数量的增多,它对于在垃圾收集时的资源利用有很大的好处。ParNew的默认开启线程数跟CPU的核数相同,同时,JVM提供参数-XX:ParallelGCThreads参数来设置垃圾收集线程数。

Parallel Scavenge收集器

Parallel Scavenge也是一款基于复制算法的young区垃圾收集器,从命名就可以知道,它的垃圾收集也是并行的多线程收集器,那么它跟ParNew收集器有什么区别呢?其实对于ParNew和CMS它们的相关优化的重点都是为了尽可能的缩短STW时间,但是Parallel Scavenge则是为了达到一个可控制的吞吐量,提高了吞吐量,也就可以更高效率的利用CPU。

注:吞吐量 = 运行时间 / (运行时间 + 垃圾收集时间)

JVM提供以下参数用于控制吞吐量:

同时,JVM还提供另一个参数-XX:UseAdaptiveSizePolicy用于控制是否要使用动态调整策略,如果使用,就不再需要指定young区的大小,eden区和survivor区的大小比例,晋升老年代对象年龄等参数,JVM会根据当前系统的运行情况性能监控信息动态调整这些参数(GC自适应调节策略)。

在使用Parallel Scavenge收集器的时候可以配合自适应调节策略,把内存管理和优化交由JVM,只需要设置好heap的最大值,再为JVM设定吞吐量相关目标,剩下的事情就可以完全放心的交由JVM去完成啦。so,自适应策略也是另一个与ParNew收集器的重要区别。

Serial Old收集器

Serial Old是Serial收集器的老年代收集器版本,它采用标记-整理算法,是一个单线程的垃圾收集器。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge的老年代收集器版本,它同样采用标记-整理算法,比起Serial Old,它是一个多线程的垃圾收集器。该收集器是在JDK 1.6之后才提出的,在此之前,Parallel Scavenge只能与Parallel Old搭配使用,由于Serial Old性能较低,所以就算使用Parallel Scavenge也并不能在整体上提升吞吐量。Parallel Old提出后该问题就迎刃而解了,在注重吞吐量的场合,可以考虑使用Parallel Scavenge + Parallel Old组合。

CMS(Concurrent Mark Sweep)收集器

CMS收集器是JDK 1.5提出的第一个真正意义上的并发收集器,它第一次实现了用户线程和GC线程并行执行。它在最大程度上减少了STW时间。

CMS收集器是基于标记-清除算法实现的,整个垃圾收集过程主要分为以下四个部分:

  1. 初始标记:标记GC Roots可以直接关联到的对象,速度非常快;

  2. 并发标记:进行GC Roots tracing,耗时较多;

  3. 重新标记:重新进行一次GC Roots tracing,重新标记也是为了修正并发标记期间因用户程序继续执行导致标记产生变化的那一部分对象,它的时间会比较长,但是比并发标记短很多;

  4. 并发清除:回收需要回收的对象内存。

整个垃圾收集过程可以和用户程序并发执行,最大程度上减少了STW时间,但是过程1和过程3仍然需要STW,所以CMS并没有完全清除STW,有些博客有些错误的理论觉得CMS没有STW,其实并不是如此。

CMS虽然是一款很好的垃圾收集器,但是它还是有一些很明显的缺点:

  1. CMS是基于标记-清除算法的垃圾收集器,如果大家对该算法还有印象的话,应该知道该算法执行结束后会产生很多的内存碎片,当空间碎片过多时往往会出现old区还有很多空间就提前触发GC。为了解决这个问题,CMS提供参数-XX:+UseCMSCompactFullCollection,用于控制进行Full GC时开启内存碎片整理压缩,此开关默认开启。内存整理后,空间碎片的问题没有了,但是由于内存整理无法并发完成,STW的时间也随之边长,为此,CMS提供另一个参数-XX:+UseCMSFullGCsBeforeCompaction设置执行多次Full GC后再压缩内存,默认值为0,表示每次执行Full GC后都进行内存压缩;

  2. CMS收集器对CPU资源敏感,在并发阶段,它虽然不会导致用户现程停顿,但是会占用一部分线程资源导致总吞吐量降低。默认CMS启动的垃圾回收线程数 = (CPU核数 + 3) / 4,当CPU核数不小于4时,垃圾回收的线程数需要占用不少于25%的CPU资源,而且随着CPU核数增加,垃圾回收占用的资源数随之减少,但是如果CPU核数少于4时,垃圾回收占用的资源就会非常大,对用户程序的影响也会很大。当然,为了解决资源占用情况,JVM推出了i-CMS(Incremental Concurrent Mark Sweep,增量式并发收集器),它采用抢占模式,GC线程、用户线程交替执行,减少了GC线程独占资源的情况,虽然会增加GC的时间,但是会减少对用户线程的影响。但是,实践证明i-CMS的效果很一般,所以很多人可能都不知道这个收集器;

  3. CMS垃圾收集器无法处理浮动垃圾,可能会出现Concurrent Mode Failure导致另一次Full GC。由于CMS的垃圾收集线程与用户线程是并行执行的,old区需要预留一部分内存给用户线程,所以CMS垃圾收集器并不会等到old区满了才触发垃圾收集。CMS提供参数-XX:CMSInitiatingOccupancyFraction设置old区使用阈值,超过该阈值时触发Full GC。如果预留内存无法满足用户程序需要,就会出现Concurrent Mode Failure,一旦出现该失败,JVM将会启动GC后备预案:临时启动Serial Old收集器进行old区垃圾回收,此时STW时间会很长。所以大家在设置参数-XX:CMSInitiatingOccupancyFraction一定要视情况而定,不要随意设置。

注:什么叫浮动垃圾(Floating Garbage)?
由于CMS垃圾收集线程与用户线程并行执行,所以在回收过程中仍然会有新的垃圾产生,如果这部分垃圾是产生在标记之后,在本次GC它们是不会被清理掉的,需要等下次GC才能被收集,这一部分垃圾就被称之为浮动垃圾。

到这里为止,6种垃圾收集器介绍告一段落,G1收集器我会单独给出博文介绍,同时给出一些性能数据分析,请大家持续关注~~~

上一篇下一篇

猜你喜欢

热点阅读