JVM系列 3-常见的垃圾收集器

1 经典的垃圾收集器

        各款经典收集器之间的关系图如下:

        上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器或老年代收集器,接下来将逐一介绍这些收集器的目标、特性、原理和使用场景,并重点分析CMS和G1两款相对复杂而又广泛使用的收集器。在介绍这些收集器各自的特性之前,先明确一个观点:虽然会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来,虽然垃圾收集器的技术在不断进步,但直到现在还没有最好的收集器出现,更加不存在“万能”的收集器,所以我们选择的只是对具体应用最合适的收集器。

1.1 Serial收集器

       Serial收集器是最基础、历史最悠久的收集器,曾经(JDK 1.3之前)是HotSpot虚拟机新生代收集器的唯一选择。这个收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾回收工作,更重要的是强调在它进行垃圾收集时,必须 暂停其他所有工作线程,直到它收集结束。这项工作是由虚拟机在后台自动发起和自动完成的,再用户不可知、不可控的情况下把用户的正常工作的线程全部停掉。下图展示Serial/Serial Old收集器的运行过程。

 

        从jdk 1.3开始,HotSpot虚拟机开发团队为消除或者降低用户线程因垃圾收集而导致停顿的努力一直持续进行,从Serial收集器到Parallel收集器,再到Concurrent Mark Sweep(CMS)和Garbage First(G1),到现在垃圾收集器的最前沿成果,Shenandoah 和ZGC等,我们看到了一个个越来越构思精巧,越来越优秀,也越来越复杂的垃圾收集器不断涌现,用户线程的停顿时间在持续缩短,但是仍然没法彻底消除。虽然如此,但是Serial依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着由于其他收集器的地方,就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有现成交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户桌面的应用场景以及近年来的部分微服务应用中,分配给虚拟机管理的内存一般不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存,桌面应用很少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一百毫秒以内,只要不是频繁发生收集,这点停顿时间对于很多用户来说是完全可以接受的。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。

1.2 ParNew收集器

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

        ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但是很重要的原因:除了Serial收集器之外,目前只有它能与CMS收集器配合使用。

        在JDK 5 发布时,HotSpot推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器——CMS收集器。这款收集器是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用于线程(基本上)同时工作。

        遗憾的是,CMS作为老年代的收集器,却无法与JDK 1.4中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial中的一个。ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或禁止它。

        可以说直到CMS的出现才巩固了ParNew的地位。但随着垃圾收集器技术的不断改进,更新件的G1收集器出现了,G1是一个面向全堆的收集器,不再需要其他新生代收集器的配合使用。所以自JDK 9开始,ParNew与CMS收集器的组合就不在是官方推荐的服务端模式下的收集器解决方案了。官方希望它能完全被G1替代,还取消了ParNew加Serial Old以及Serial以及Serial加CMS这两组收集器组合的支持,并直接取消了 -XX:+UseParNewGC参数,这意味着ParNew和CMS以后只能互相搭配使用。

1.3 Parallel Scavenge收集器

        Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,能够并行收集的多线程收集器等。从表面上看和ParNew非常相似,那么它有什么特别之处呢?

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

        如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费100分钟,其中垃圾收集花费1分钟,那吞吐量就是99%。停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的相应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。 

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

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

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

        由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称为“吞吐量优先收集器”。除了上面两个参数,还有一个参数-XX:+UseAdaptiveSizePolicy值得注意。这是一个开发参数,当这个参数被激活,就不需要人工指定新生代、Eden和Survivor区的比例、晋升老年代对象大小等细节参数了,虚拟机会根据当前系统的运行情况,收集性能监控信息,动态调整这些参数以提供最合适的自适应的调节策略。

1.4 CMS收集器

        CMS收集器是一种以获取最短回收停顿为目标的收集器。目前很大一部分的Java应用集中在互联网或者基于浏览器的B/S系统的服务端上,这类应用通常会关注服务的响应速度,希望系统停顿时间尽可能短,已给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。

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

        1、初始标记

        2、并发标记

        3、重新标记

        4、并发清除

        其中初始标记、重新标记这两步仍需要“stop the world”。初始标记仅仅只是标记一下GC Roots 能直接关联到的对象,速度很快;并发标记阶段就是说从GC Roots的直接关联对象开发遍历整个对象图的过程,这个过程耗时较长但不需要停顿用户进程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

        由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。下图可以比较清楚地看到CMS收集器的运行步骤中并发和需要停顿的阶段。

        CMS是一款优秀的收集器,它最要的优点:并发收集、低停顿。然而,CMS虽然是HotSpot虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度,因为至少有三个明显的缺点:

        1、CMS收集器对处理器资源非常敏感。事实上,面向并发设计的程序都对处理器资源比较敏感。在 并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,减低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量+3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足4个时,CMS对用户程序的影响就可能变得很大。如果应用本身的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致程序的执行速度忽然大幅降低。为了缓解这种情况,虚拟机提供了一种称为“增量式并发收集器”的CMS收集器变种,所做的事情和以前单核处理器年代PC操作系统靠抢占式多任务来模拟多核并行多任务的思想一样,是在并发标记、清理的时候让收集器线程、用户线程交替运行。尽量减少垃圾收集线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响会降低,直观感受就是速度变慢的时间更多了,但速度下降幅度没那么明显。实践证明增量式的CMS收集器效果一般,从JDK 7开始,I-CMS模式已经被声明为“deprecated”,即已过时不在提倡用户使用,到JDK 9发布后I-CMS模式被完全废弃。

        然后,由于CMS收集器无法处理“浮动垃圾”,有可能出现“con-current mode failure”失败进而导致另一次完全“stop the world”的full GC 的产生。在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留带下一次垃圾收集时再清理掉。这部分垃圾就称为“浮动垃圾”。同样,也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够的内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运行使用。在JDK 5 的默认设置下,CMS收集器当老年代使用了68%的空间后会被激活,这是一个偏保守的设置,如果在实际应用中老年代增长并不是太快,可以适当调高参数 -XX:CMSInitiatingOccu-pancyFraction的值来提高CMS的触发百分比,降低内存回收频率,获得更好的性能。到了JDK 6,CMS收集器的启动阈值已经默认提升到92%。但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”,这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。所以参数 -XX:CMSInitiatingOccupancyFraction设置的太高将会很容易导致大量的并发失败发生,性能反而降低,用户应在生茶环境中根据实际应用情况来权衡设置。

        还有最后一个缺点,在开头提到,CMS是一款基于“标记-清除”算法实现的收集器,这将意味着收集结束时会有大量空间碎片产生。空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发Full GC的情况。为了解决这个问题,CMS收集器提供了一个 -XX:+UseCMS-CompactAtFullCollection开关参数(默认是开启的,此参数从JDK 9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的。这样空间碎片问题是解决了,但停顿时间又会边长,因此虚拟机设计者们还提供了另一个参数 -XX:CMSFullGCBefore-Compaction(此参数从JDK 9 开始废弃),这个参数的作用是要求CMS收集器在执行过若干次不整理空间的Full GC后,一下次进入Full GC前会先进行碎片整理。

1.5 Garbage First 收集器

        Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。早在JDK 7刚刚确立项目目标,Oracle公司制定的JDK 7 RoadMap 里面,G1收集器就被视作JDK 7中HotSpot虚拟机的一项重要进化特征。从JDK 6 update 14开始就有Early Access 版本的G1收集器供开发人员实验和试用,但由此开始G1收集器的“试验状态”持续了数年时间,直至JDK 7 update 4,Oracle才认为它是达到了足够成熟的商用程度。到了 JDK 8 update 40的时候,G1提供并发的类卸载的支持,补全了期计划功能最后一块拼图。这个版本以后的G1收集器才被Oracle官方称为“全功能的垃圾收集器”。

        G1是一款主要面向服务端应用的垃圾收集器。HotSpot开发团队最初赋予它的期望是未来可以替代换掉JDK 5 中发布的CMS收集器。现在这个期望目标已经实现过半了,JDK 发布之日,G1宣布取代Parallel Scavenge加Parallel Old 组合,称为服务器模式下的默认垃圾收集器,而CMS则被声明为不推荐使用的收集器。如果对JDK 9及以上版本的HotSpot虚拟机使用参数 -XX:+UseConcMarkSweepGC来开启CMS收集器,用户回到一个警告信息,提示CMS未来将会被废弃。

        但作为一款曾被广泛运用过的收集器,经过多个版本的开发迭代后,CMS的代码与HotSpot的内存管理、执行、编译、监控等子系统都有千丝万缕的联系,这是历史原因导致的,并不符合职责分离的设计原则。为此,规划JDK 10功能目标时,HotSpot虚拟机提出了“统一垃圾收集器接口”,将内存回收的“行为”与“实现”进行分离,CMS以及其他收集器都重构成基于这套接口的一种实现。以此为基础,日后要移除或者加入某一款收集器,都会变得容易的多,风险也可以控制。

        作为CMS收集器的替代者和继承人,设计者们希望做一个款能够建立起“停顿时间模型”的收集器,停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间大概率不超过N毫秒这样的目标,这几乎已经是实时Java的中软实时垃圾收集器特征了。

        在G1收集器出现之前的所有其他收集器,垃圾收集的范围要么是整个新生代,要么是整个老年代,或者是整个Java堆。而G1可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。        

        G1开创了基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间、或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活过一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

        Region还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数 -XX: G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于哪些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分进行看待。如下图所示:

        虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一些列区域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是整个Region大小的整数倍,这样可以有计划地 避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需的时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数 -XX:MaxGCPauseMillis指定,默认是200毫秒),优先处理回收价值收益最大的哪些Region,这就是“Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率

        

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

geminigoth

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值