jvm常见垃圾回收器


前言

垃圾收集器的终极目标:提高吞吐量,降低延时,减少停顿。与之相对的则是更加复杂的设计和更多资源的消耗。


一、常用垃圾回收器分类

在这里插入图片描述

按照所负责回收区域划分:老年代回收器 ,新生代回收器

按照gc执行的资源成本划分: 多线程回收 ,单线程回收

按照与用户线程关系划分:与用户线程并发 ,需暂停用户线程
在这里插入图片描述

二、常见垃圾回收器

1.Serial收集器

光看名字就可以知道这个回收器的主要特性,这是一个单线程的垃圾回收器,即执行垃圾回收操作时只有一个gc线程工作,最重要的是在他执行gc过程中,必须全程暂停用户线程,也就是经常说的Stop the world问题。以下是Serial执行流程的示意图,比较好理解
请添加图片描述
这是jvm最早期的收集器,虽然STW问题是确实是一个诟病,但是这种方案实现起来较简单,占用资源也是最小的,对应新生代内存比较小的应用中,相对比较适用,比如运行在客户端模式下的虚拟机。
![请添加图片描述](https://img-blog.csdnimg.cn/4e2517fa47c841448f287d2153acc345.png

2.ParNew收集器

和Serial收集器不同的是,这是一款多线程收集器,即有多条gc线程同时进行垃圾回收工作,剩余其他的特点和Serial收集器基本无异,比如控制参数,回收算法,STW问题等,作为一款新生代收集器,常用来和CMS搭配使用,ParNew是激活CMS后默认的新生代收集器。
由于多线程执行的原因,在单核cpu下,由于线程上下文的切换,该收集器的效果甚至不如Serial收集器,所以在多核心的cpu中,还有一些用武之地,但遗憾的是,在hotspot中该收集器已经推出历史舞台,当初叫人家小甜甜,现在却喊人家牛夫人,新人胜旧人,会有更高级的收集器来替代它。
请添加图片描述

3.Parallel Scavenge收集器

这也是一款新生代收集器,而且与ParNew实现上差别不大,不同的是,该收集器的侧重点是虚拟机的吞吐量,吞吐量很好理解,即用户代码的执行时间占整个系统运行时间的比重,其中就包括gc时间。如果虚拟机运行期间gc消耗的时间,占用的资源相对较高,那吞吐量自然也就会下降。而Parallel Scavenge关注的就是吞吐量的可控,尽可能的减少gc时间,让用户线程执行更长的时间,一些交互性比较弱的应用,比如科学计算、批处理任务、订单流转等。
为了实现这个目标,Parallel Scavenge提供了一些参数:
-XX:MaxGCPauseMills: 即“最大gc停顿毫秒值” , 该值设置的大小是以新生代回收空间为代价的,设置的值越小,意味着所能回收的空间也会越小,相应的gc频率自然会提高,这个时候未必会提高吞吐量,具体的设置多少,需要根据应用的实际场景来决定
**-XX:GCTimeRatio:**该参数的范围是大于0小于100的整数,意义是用户程序的运行时间和垃圾回收时间的比例,比如设置为99,那最大允许的垃圾回收时间为1%。
**-XX:+UseAdaptiveSizePolicy:**直译过来就是“自适应大小策略”,当这个参数被激活后,就不需要指定新生代、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等参数,虚拟机会根据当前的运行状态,收集性能监控信息,动态调整这些参数以提供最合适的停顿时间和最大吞吐量。

4.Serial Old收集器

Serial收集器的老年代版本,采用标记-整理算法,与Serial一样,也是供虚拟机的客户端模式使用,有两个用途:一种是JDK5之前与Parallel搭配使用(1.5之后与Parallel Old搭配使用),另外一种就是作为CMS收集器发生失败的备选方案,在并发收集发生Concurrent Mode Failure时使用。
请添加图片描述

5.Parallel Old收集器

Parallel Scavenge的老年代版本,从名字也可以看出,这是一款与Parallel Scavenge搭配使用的收集器,也是基于标记-整理算法实现,其缺点与Parallel Scavenge基本无异,多线程收集,停顿用户线程等等
请添加图片描述

-XX:+UseParallelOldGC:指定使用Parallel Old收集器

6.CMS收集器

前边的几款收集器,都是实现相对简单的收集器,虽然都有各自的一些特点,但不出意外的他们都存在一个缺陷,即gc过程中需要全程暂停用户线程,而CMS则是收集器家族中第一款“真正意义”上的“几乎”不用停顿用户线程的一款多线程收集器。CMS全称Concurrent Mark Sweep ,直译过来就是并发标记清理,也就是说在标记阶段和清理阶段CMS是与用户线程并发工作的,其中包含四个主要步骤:

  1. 初始标记 (initial mark)
  2. 并发标记 (concurrent mark)
  3. 重新标记 (remark)
  4. 并发清除 (concurrent sweep)

初始标记阶段其实就是标记以下gc roots能直接引用到的对象,这一阶段是要停顿用户线程的,但相对速度比较快
接下来就是并发标记,这一段标记过程全程与用户线程并发执行,不需要停顿用户线程。
不停止用户线程对象引用发生变化怎么办?没错,就是重新标记,CMS使用的是增量更新算法实现的重新标记。按照并发可达性分析原理来看,这一段是一定要停顿线程的,不然永无休止的运行下去。但是相对于整个对象图的扫描来看,毕竟需要重新标记的这部分对象占极少数,速度也是相对较快,比初始标记阶段要长一些。
最后就是并发清除,因为重新标记完后,被标记死亡的对象已经没有引用价值,且不需要移动对象,这一阶段是可以直接并发清除的,从这里也可以看出CMS是基于并发-清除算法的,当然也会有碎片产生。
下图示意了CMS整个执行流程:
请添加图片描述

相对前边几款收集器来说,CMS的优势重点在于与用户线程并发,减少用户线程的停顿时间,更适合一些交互性较强的应用,但是它仍然有一些比较明显的缺点

  1. 首先就是它的实现相对前几款收集器要复杂一些,复杂往往意味着资源消耗变大。虽然不会导致用户线程长时间停顿,但是却抢占了更多的资源,直接表现为吞吐量要来的低一些。随着cpu核心数的减少,消耗的资源占比提高,进而影响用户线程的执行效率,所以CMS是一个资源敏感的收集器,越高的资源配给,CMS相对占用资源就越少。

为了尝试解决这个问题,中间出现了一种“增量式并发收集器i-CMS”,在执行gc过程中让gc线程和用户线程交替运行,减少垃圾收集器独占资源的时间,整个gc时间更长,但对用户线程影响相对小一些,直观的表现就是速度慢的时间变多了,但是速度下降幅度没有那么明显,该收集器在jdk7已被弃用,长痛也是痛,意义不大。

  1. 由于CMS在工作期间,用户线程此时也在工作,内存中还将继续分配对象,新分配的对象在当前gc过程中全部视为存活,也就是在这段时间内,即使新分配的对象瞬间成为了垃圾,垃圾收集器也没有办法在当前gc过程中将他们扫描到。如果在这个过程中分配的对象需要空间大于堆剩余空间,就会出现Concurrent Mode Failure(并发失败)问题。CMS采用的方案,也是无奈之举,即在新对象无法重新分配内存时,暂停用户线程运行,启动Serial Old收集器完成收集,这也就会大大增加用户线程的停顿时间。

为了保证在并发收集过程中,尽可能的保证新对象分配能够获得足够的内存空间,所以CMS不能等待老年代空间快要满了才去开始收集任务,垃圾回收的时候必须要保证有足够的的空闲空间,可以通过参数来设置当老年代空间到达多少后CMS开始工作,-XX:CMSinitiatingOccupancyFraction,该值设置需要根据实际的应用情况来考虑,太低可能会导致频繁并发收集,降低吞吐量,太高可能会导致大量Concurrent Mode Failure产生,导致gc时间提高,应用性能降低。

  1. 基于标记-清除算法的都逃不过一个最明显的问题,就是内存碎片,CMS亦是如此。碎片化严重,导致一些大对象无法找到连续的内存地址使用,这个时候将不得不触发一次Full GC来回收内存。

CMS提供了两个参数来解决内存碎片的问题,这两个参数在JDK9被废弃。
-XX:+UseCMSCompactAtFullCollection 直译过来就是在Full gc的时候进行内存整理,整理过程中要移动对象,无法与用户线程并发,停顿时间会变得更长
-XX:+CMSFullGCsBeforeCompaction 要求CMS在执行若干次不整理空间的Full gc之后,下次进入Full gc前先进行碎片整理,默认值0 ,每次进入Full gc都进行内存碎片整理。╮(╯▽╰)╭

7.G1收集器

对比前几款商用的收集器,G1收集器最大的不同就是没有采取固定的内存分代的设计原则,也就是说在设计上,不存在Minor gc,Major gc,Full gc这种对特定区域的回收行为,G1采用了基于Region的内存分布形式,有点像磁盘中的blocked块。
具体做法是将堆划分为大小相等的Region独立区域,每个Region根据需要都可以称为新生代的Eden区、Suivivor区,或者老年代区,收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象,还是已经存活了一段时间的对象都能获得良好的收集效果。
同时Region还有一类特殊的humongous区域,专门用来存储大对象,只要超过了一个Region容量的一半就会被认为是大对象,如果对象长度超过了Region的大小,则会使用多个连续的humongous来存放,G1的大多数行为会把该区域当做老年代来看待。

-XX:G1HeapRegionSize //设置Region大小,范围1-32MB

G1依然保留了新生代和老年代的概念,但是在实际内存中新生代和老年代不是固定的,他们是一系列的“动态集合”,通过这个动态集合建立一个可预测的停顿时间模型,换句话说,G1的回收是建立在这个停顿时间模型上的。每次回收都是以Region为单位,让G1去跟踪每一个Region中垃圾的“价值大小”,比如回收空间大小和回收所需的时间,然后通过这个价值大小维护一个优先级的列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值最大的那些Region,这种划分Region空间以及具有优先级的区域回收方式,使得G1在有限时间内尽可能获得更高的收集效率。

停顿时间模型:能够支持指定在一个长度M毫秒的时间片段内,消耗在gc的时间大概率不超过N毫秒这样的目标

在这里插入图片描述
通过以上的图,各个区域被拆分成了一个个Region,组成了一个动态的集合,不再严格的划分新生代、老年代,每个区域既可以是新生代,也可以是老年代。
尽管G1相对于前几款回收器,已经表现得更为先进,但是依然还是有一些问题:

  1. 堆被分为若干个Region后,对象之间的跨代引用问题,G1也是采用了记忆集的方式解决Region间跨代引用问题,但是由于Region数量比较多,整体维护起来所消耗的内存更高,对于一些内存较小的应用,gc所造成的负担相对会更高。
  2. 与CMS一样,如果在gc执行过程中,内存回收的速度跟不上内存分配的速度,也会出现Concurrent Mode Failure问题,导致G1被迫暂停用户线程,执行Full gc流程。

在垃圾回收过程中,G1会为每一个Region中设置两个名为TAMS(Top at Mark Start)的指针,该指针标识了该Region中的一段区域,这段区域专门用作在gc过程中新对象的创建分配。
G1使用“衰减平均值”来表示各个Region的“最近的平均状态”,换句话说,Region的统计状态越新越能决定其回收价值。
G1重新标记采用的是原始快照的方式

总体上看G1执行过程可分为以下四步:

  • 初始标记:标记gc roots直接关联的对象,修改TAMS指针的值,目的是下阶段并发回收时,能够继续正常的分配对象。这个阶段需要停顿用户线程,但是相对比较短暂。
  • 并发标记:从gc roots开始对对象图进行可达性分析,找到有回收的对象,这个阶段与用户线程并发。
  • 最终标记:对并发标记时引用发生变化的对象重新标记,通过原始快照的方式,毫无疑问,这阶段需要短暂暂停用户线程
  • 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,结合用户指定的停顿时间制定回收计划,可以自由选择任意多个region构成回收集,然后把决定回收的那一部分region中存活的对象复制到空的region中,再清理掉整个旧region空间,由于要移动对象,所以这个阶段也必须暂停用户线程,有多个收集器线程并行完成 。

请添加图片描述

通过以上过程可以看出,除了并发标记阶段,其他阶段都需要暂停用户线程,所以G1主要目标是在延迟可控的情况下获得尽可能高的吞吐量,希望在延迟和吞吐量之间达到一个平衡。

设置-XX:MaxGCPauseMills时,需要结合实际情况,如果设置的太高,会增加用户线程的停顿时间,整个应用的延时就会增加。如果设置的太低,则会导致G1在执行gc时不得不减少回收集的大小,如果gc速率赶不上新对象分配的速率,这会导致Full GC发生,反而会降低性能。

与CMS标记-清除算法不同,G1从整体上看是基于标记-整理算法,从局部Region看是基于标记-复制算法,这两种算法都不会产生内存碎片,有利于应用程序的长时间运行。但是由于G1的内存分布更为复杂,相对CMS会消耗更多的内存和cpu资源。

在执行负载上,CMS只用到了写后屏障来更新卡表,而G1除了使用写后屏障更新卡表之外,还需要写前屏障来记录并发标记时引用的更新状态(原始快照需要记录引用变化前的状态),相比起CMS的增量更新算法,G1原始快照虽然能够减少重复标记阶段的消耗,减少了用户线程的停顿时间,但是由于需要跟踪引用的更新状态,会消耗更多的运算资源,所以G1不得不将写前屏障和写后屏障放入队列进行异步操作。

总结

以上是7款目前已经商用的垃圾回收器,除此之外还有更多正在实验阶段更强悍的回收器,比如Shenandoah,ZGC,由于篇幅有限加之两者更为复杂,留作下篇单独做记录总结。能力有限,如有问题,请看官们及时指正~

文中插图引用自:深入理解Java虚拟机第三版 - 周志明著

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值