JVM垃圾收集之三:垃圾收集器

本文详细介绍了Java虚拟机中的垃圾收集器,包括Serial+SerialOld、ParallelScavenge+ParallelOld、ParaNew+CMS和G1。重点讲述了各收集器的特点、如何实现高吞吐量以及它们之间的优缺点,展示了垃圾收集器发展的脉络。
摘要由CSDN通过智能技术生成

目录

概述

垃圾收集器

1. Serial+Serial Old

2. Parallel Scavenge + Parallel Old

Parallel Scavenge收集器特点

Parallel Scavenge如何实现高吞吐量的呢?

3.ParaNew + CMS

CMS特殊介绍

CMS的三个明显的缺点

4. G1

G1运作过程

G1和CMS作比较


概述

        上一篇《垃圾收集算法》垃圾收集之二:垃圾收集算法-CSDN博客 我们讲了垃圾收集算法相当于是内存回收的方法论,今天讲的垃圾收集器就是内存回收的实践者,

垃圾收集器

上图共有7中分代垃圾收集器,分别是年轻代的Serial,ParNew,Parallel Scavenge;老年代的CMS,Serial Old,Parallel Old;逻辑分代,物理不分代的G1。

        由于垃圾收集器分为年轻代和老年代,除了G1之外其他垃圾收集器必须成对组合进行使用,收集器之间存在连线,就说明它们可以搭配使用,其中三条是废弃的搭配,常见的搭配为 Serial+Serial Old;ParNew+CMS;Parallel Scavenge+Parallel Old;下面我们就根据jvm的垃圾收集器发展顺序介绍每个收集器,同时也能看出垃圾收集器的发展脉络。

1. Serial+Serial Old

        Serial是最基础的垃圾收集器,在计算机术语中翻译为串行的,从字面意思可以看出Serial是一个单线程垃圾收集器;Serial垃圾收集器工作在新生代使用标记-复制算法,整个回收过程STW。 Serial Old也是单线程,整个回收过程STW,和Serial不同的点在于:工作在老年代,使用标记-整理算法。

        简单的理解:你和几个朋友在房间里嗑瓜子,瓜子皮扔的满地都是,你妈看不下去了,说停!你就坐在椅子上乖乖不能嗑瓜子(STW),如果你们继续嗑瓜子,你妈永远打扫不完房子。

        以当前开发者的目光Serial+Serial Old组合(后文称:Serial组合)已经是过时多年的“鸡肋”了,食之无味,弃之可惜,Serial组合刚出现时,程序的内存都是几十兆,多点的几百兆,CPU也都是单核处理器,在这种资源受限的环境,Serial组合可谓如鱼得水,它既简单又高效!这是Serial组合适用的场景:单核,内存几十兆~上百兆。

Serial/Serial Old收集器运行示意图:

2. Parallel Scavenge + Parallel Old

        Serial组合在单核,内存几十上百的资源受限场景工作还可以,但是随着硬件的发展,服务器内存快速增长为几G,十几个G,CPU变为多核,此时Serial显得有点吃力了。

        所以Parallel Scavenge + Parallel Old组合(后文称:PS+PO组合)就出现了,从字面意思我们知道Parallel Scavenge是并行清除,即多个线程并行清除,其所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等和Serial收集器完全一致。Parallel Old是Parallel Scavenge的老年代版本,多线程收集,采用标记-整理算法。

        简单理解:你家原来30平筒子楼,你老妈三下五除二把瓜子皮打扫完了,你和朋友们就可以继续吃瓜子,但现在时代变了,你家住上了100平的大house,你和朋友把瓜子皮扔的满屋子都是,你妈的扫了半个小时,终于扫完了,你们半小时只能等着不能吃瓜子,很明显你们都对做卫生速度不满意。你妈为了体现自己在家的统治力,叫上了你老爸,你哥,你姐,同时开始打扫,三下五除二把瓜子皮打扫完了。

Parallel Scavenge / Parallel Old收集器的工作过程如图:

Parallel Scavenge收集器特点

        Parallel Scavenge的目标则是达到一个可控制的吞吐量(Throughput),垃圾收集之二:垃圾收集算法-CSDN博客 中我们已经介绍过吞吐量的计算方法:

如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。高吞吐量不在乎STW最大停止时间,而是在乎让最多比例的时间花费在用户代码上。高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务

Parallel Scavenge如何实现高吞吐量的呢?

        待补充......

3.ParaNew + CMS

        ParaNew和Parallel Scavenge几乎一样,多线程收集,在年轻代使用拷贝算法,使用过程STW,唯一的区别是ParaNew支持和CMS配合使用,其他年轻代垃圾收集器不支持。

CMS特殊介绍

        CMS(Concurrent Mark Sweep)翻译过来就是:并发标记清除,老年代垃圾收集器,通过名字可以知道它在老年代使用了标记-清除算法。CMS是一种以获取最短回收停顿时间为目标的收集器CMS出现前所有的垃圾收集器都要STW,用户线程全部停止,垃圾收集器才能工作,CMS把一段长STW变成了两段短而小的STW,极大地缩短了垃圾收集期间STW的时间,并且让垃圾标记和用户线程并发运行,是一款破旧立新的垃圾收集器,以后垃圾收集器都采用了并发收集的策略。

        简单理解:CMS 是一个承上启下的特殊产物,好比历史上秦朝一样,秦之前都是邦国林立,秦以后天下一统;CMS之前都是STW,CMS之后都是并发垃圾收集(并发收集),减少STW时间(低停顿)。还有秦朝作为革新者二世而亡,CMS作为HotSpot虚拟机追求低停顿的第一次成功尝试,也没摆脱这个宿命,因为本身有很多致命问题,没有一个JDK版本默认使用CMS。

        简单理解:你们家更阔了,在上海外滩盖了1000平的江景别墅,你和朋友在家里嗑瓜子,每当你们扔的家里都是瓜子皮,你妈妈就带着队伍来打扫,你们就不能吃了,你和朋友都不开心:“就不能我们一边吃你们一边打扫吗?”你妈说可以:“从现在开始,你们就边吃(但是只能去厕所吃),我们边打扫,而且还能把房间打扫干净”。

        CMS收集器的运行过程包括四个步骤:初始标记,并发标记,重新标记,并发清理。其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。因为CMS采用的三色标记算法,在三色标记算法过程及原理(Tri-color marking)-CSDN博客中已经讲过三色标记,有需要可以点击查看。

CMS的三个明显的缺点

        1,对处理器资源敏感,事实上面向并发设计的程序都对处理器资源比较敏感,因为在并发阶段,虽然不会STW,但因为GC会占用一部分处理器资源,导致应用程序变慢,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量+3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。CMS适合在4核及以上的CPU的服务器工作

        2,CMS收集器无法处理“浮动垃圾”(Floating Garbage),在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,下一次垃圾收集时再清理掉。

        同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,在JDK5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在实际应用中老年代增长并不是太快,可以适当调高参数-XX:CMSInitiatingOccu-pancyFraction的值来提高CMS的触发百分比,降低内存回收频率,获取更好的性能。到了JDK 6时,CMS收集器的启动阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。所以参数-XX:CMSInitiatingOccupancyFraction设置得太高将会很容易导致大量的并发失败产生。

        简单理解:你妈正在带领队伍打扫卫生,给你们关到了两平米厕所嗑瓜子,打扫卫生时你们不能出来,如果你们嗑瓜子太快,很快厕所也满了,你妈就生气了,大吼一声:停!你和朋友不能嗑瓜子了,同时你爸,你哥,你姐都被你妈踢了出去,然后你妈一个人拿着扫把打扫1000平江景别墅,打扫完天都黑了。

        总结:由于浮动垃圾以及并发收集必须要求老年代留有足够的空间,如果并发收集期间出现老年代空间不够,CMS就会出现“并发失败”(Concurrent Mode Failure),并且STW,启用备选方案Serial Old来进行老年代垃圾收集,面对巨大的内存空间,Serial Old头皮发麻,一口气干了一天才完事。

        3,CMS(Concurrent Mark Sweep)采用标记-清除算法,必然产生内存碎片,当碎片足够碎小时,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前大对象,而不得不提前触发一次Full GC的情况。

        CMS本身并不执行压缩操作,因此即使在Full GC之后,如果继续使用CMS作为老年代的收集器,碎片问题可能很快就会再次出现。这是因为CMS的清理机制没有改变,它仍然会在每次收集后留下碎片。

        为了缓解这个问题,从JDK 7 Update 72开始,CMS引入了一个可选的特性,允许在Full GC之后进行一次压缩操作。这个行为可以通过-XX:+CMSClassUnloadingEnabled-XX:+CMSFullGCsBeforeCompaction这两个JVM参数来控制。CMSFullGCsBeforeCompaction参数允许你指定在进行压缩之前允许多少次Full GC发生,默认值是0,意味着每次Full GC之后都会进行压缩。

        但是,需要注意的是,CMS的压缩操作也是耗时的,并且会导致应用程序停止,这与CMS设计的初衷(最小化停顿时间)相违背。因此,如果碎片问题持续存在并且影响到了系统的性能,可能需要考虑切换到不同的垃圾收集策略,比如使用G1收集器,它在设计上就考虑到了碎片问题,并且能够同时进行清理和压缩。

4. G1

        Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,HotSpot开发团队最初赋予它的期望是未来可以替换掉JDK5中发布的CMS收集器。JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。

        作为CMS的继承者,设计者发明G1的初衷是建立一个“可预测的停顿时间模型”(Pause Prediction Model):能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N(默认200ms)毫秒这样的目标。

        举例:假设我们正在运行一个实时交易系统,其中每个交易请求都需要在500毫秒内得到响应(这是M)。为了保证这一点,我们设置了G1垃圾收集器的停顿时间目标为N=200毫秒。这意味着在任何500毫秒的时间段内,垃圾收集器的活动不应该导致超过200毫秒的停顿。

        为了实现这个目标,从原来的设计思想上实现创新:G1之前的垃圾收集器,收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC);G1跳出原来思想,开启了局部收集的思路,它可以面向对内任何部分来组成回收集(Collection Set或CSet),回收的标准也不是根据分代,而是哪块内存垃圾数量最多,回收收益最大,这也是Garbage First的由来。如何实现这个面向局部收集的设计思想呢?

        G1开创了收集器基于Region的内存布局形式。G1是逻辑分代,虽然还有年轻代,老年代的分代逻辑,但是不会像之前的垃圾收集器在物理内存上将堆内存分为年轻代,老年代,而是提出Region概念,它把连续堆内存分为多个大小相等的独立区域Region[注1],每个区域都可以根据需要扮演新生代的Eden区或Survival区或老年代的Old区,大对象放到特殊的Houmongous区域。

        G1将Region当做单次回收的最小单元,每次回收到的空间都是Region大小的整数倍。G1收集器会跟踪每个Region中垃圾堆积的“价值”大小(价值即:回收所得空间大小和回收所需时间的经验值)然后维护一个优先级列表,每次回收根据用户设置的收集停顿时间(-XX:MaxGCPauseMilis指定,默认值200ms)回收价值收益最大的Region。它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量

G1运作过程

        1)初始标记:标记GC-Roots能直接关联到的对象;修改TAMS指针的值,在并发阶段,用户线程能够正确地在可用的Region中分配对象。此阶段用户线程需要很短的STW。

        2)并发标记:从GC-Roots关联到的对象开始对堆中对象进行可达性分析,递归扫描整个堆的对象图,耗时较长,但GC线程和用户线程并发运行;原始快照STAB方式记录在并发过程中有引用变动的对象。

        3)最终标记:此阶段用户线程需要很短的STW,处理并发标记阶段记录下来的STAB记录。

        4)筛选回收:更新所有Region统计的数据,对各个Region的回收价值和成本进行排序,根据用户设定的暂停时间来制定回收计划,选择需要回收的Region组成回收集合(Collection Set)CSet,将CSet中的Region中存活的对象复制到空的Region中,再清理掉整个Region,因为涉及到对象的拷贝,必须暂停用户线程,产生STW。

        简单理解:你家太阔了,买下了美国国家广场当做你家,你家里有很多房子,白宫,历史博物馆,杰斐逊纪念馆、罗斯福纪念馆等等,你和你的朋友在白宫嗑瓜子,扔的满地都是瓜子皮,你妈你爸等说:咱家太大了,我们不追求一次性把家打扫干净,你妈你爸等的扫把没有那能力,你们可以边吃边看我们打扫,但是我们只追求打扫的速度快于你们吃瓜子的速度就行了。你说:妈,瓜子皮影响游客参观,限你在200ms内完成打扫工作,你妈你爸说:没问题;他们拿出笔记本,把打扫的房子花费时间少的并且打扫完面积大的优先写在笔记本最上面,然后把笔记本最上面能在200ms内打扫完的房子圈起来,并且抢了哈利波特的扫把,把画圈内的房子打扫完了。

注1:

        每个Region区域大小可以通过-XX:G1HeapRegionSize设定,取值范围在1MB~32MB,且应为2的N次幂,Region默认大小为2MB,当一个对象大小超过了Region容量的一半,就被判定为大对象,大对象放到特殊的Houmongous区域,Houmongous可以当做老年看待。

G1和CMS作比较

        G1和CMS作比较的优点:

                1)可以设置最大停顿时间。

                2)清理Region时使用复制算法,不会产生内存碎片。

                3)8G及以上内存,G1性能优于CMS。

        G1和CMS作比较的缺点:

                1)G1在内存占用方面比较高,G1至少要耗费大约相当于Java堆容量10%至20%的额

外内存来维持收集器工作。

                2)6G及以下内存,G1性能劣于CMS(2014年)。

书籍:深入理解Java虚拟机:JVM高级特性…佳实践(第3版)周志明。

声明:侵权必删,本文无任何功利,给知识传播多一扇窗。

  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值