JVM学习笔记(八)———经典垃圾收集器(2)

CMS和G1是Java HotSpot虚拟机的两种垃圾收集器,各自有独特优势。CMS以最短停顿时间为目标,采用并发标记清除算法,但在浮动垃圾和并发失败时存在问题。G1则引入了区域划分和停顿时间模型,通过优先回收高收益区域实现低停顿,同时避免碎片化,但内存占用和复杂度较高。两者适用于不同场景,需根据应用需求选择。

经典垃圾收集器

(本篇只包含CMS和G1收集器,其他收集器与收集器分类参考上一篇

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一款以最短回收停顿时间为目标的垃圾收集器,使用标记-清除算法进行垃圾回收。一般来说,需要与用户频繁交互的场景都需要尽可能短的回收停顿时间,以换取更好的用户交互体验,CMS非常符合这类场景。

CMS运作过程

CMS的运作过程相较前面的五款收集过程更为复杂,整个过程包括四个阶段

  1. 初始标记(CMS initial mark)(需要暂停用户线程)
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark)(需要暂停用户线程)
  4. 并发清除(CMS concurrent sweep)

四个阶段中,只有初始标记和重新标记需要暂停用户线程,其他两个阶段则可以与用户线程交替执行,并发式垃圾收集是区别于前文提及的五款垃圾收集器的重要标志之一。四个阶段具体工作如下

  1. 初始阶段仅需要标记与GC Roots直接关联的对象,速度很快。
  2. 并发标记阶段负责从直接关联对象开始遍历整个对象图,此过程耗时较长但是是并发执行,可以与用户线程交替执行。
  3. 重新标记,重新标记阶段会对在并发标记阶段,因用户线程持续执行导致标记产生变动的那部分对象进行修正(增量更新),此阶段用时比初始阶段长一些,但远比并发标记阶段短。
  4. 并发清除,回收掉标记阶段被判定为死亡的对象,由于不需要整理内存空间,因此也不需要暂停用户线程。

四个阶段耗时最长的并发标记和并发清除都与用户线程一同工作,所以总体上可以看作,CMS的垃圾收集行为是与用户线程并发进行的。CMS收集器运行示意图如下:
在这里插入图片描述

CMS的问题与缺陷

CMS收集器是HotSpot虚拟机追求低停顿的第一次成功尝试,但同时CMS也存在以下明显的缺点或问题:

  1. 垃圾收集过程中,应用程序变慢:在垃圾收集过程中,虽然CMS不会导致长时间的用户线程停顿,但却因为占用了一部分线程而导致在垃圾收集阶段时,应用程序运行变慢,可以理解为,CMS处理器牺牲了吞吐量换取了低停顿。处理器核心数越少这种使应用程序变慢的问题体现的就越明显(处理器核心数少了也就相当于垃圾收集器占用的核心数比例高了)。
  2. 浮动垃圾:在并发清除阶段,用户线程还在继续运行,这意味着还会不断有新的垃圾对象产生,而CMS无法对这部分对象进行标记,CMS无法处理的这部分浮动垃圾,只能等到下一次垃圾收集时再处理。
  3. 并发失败:因为垃圾收集与用户线程是同时运行的,所以CMS不能像其他收集器一样等待老年代快满了才开始收集,必须预留一部分空间供并发的用户线程使用,如果在垃圾收集运行过程中,已经无法为新对象分配内存空间,将出现一次并行失败,这是虚拟机不得不使用逃生门,冻结用户线程的执行,临时启用Serial Old收集器来进行老年代垃圾收集。
  4. 空间碎片化:由于CMS使用标记-清除算法,并不具备内存空间压缩能力,当空间碎片化过于严重以至于无法为大对象分配内存空间时,就不得不出发一次Full GC来解决。

G1收集器

Garbage First简称G1收集器,是垃圾收集技术发展历史上里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。

G1是一款面向服务端的收垃圾收集器,在G1收集器出现之前的所有收集器都是分代或整堆收集,而G1采用了新的思路,G1可以面向堆内存任何部分来组成回收集,回收的衡量标准也不再是分代(G1仍遵循分代收集理论),而是收集哪个区域的垃圾收益最高,这就是G1收集器的MixedGC。

基于Region的堆内存布局

G1开创了基于Region的堆内存布局,G1不再坚持固定大小和固定数量的分代区域划分,而是将连续的Java堆划分为若干个大小相同的独立区域(即Region,物理地址上允许不联系),每一个区域都可以根据需要扮演新生代的Eden、Survivor空间或老年代空间,G1对扮演不同角色的Region采取不同的收集策略。

Region中还有一类特殊的Humongous区域,专门用来存储大对象,G1认为大小超过了一个Region容量的一半的对象都可判定为大对象,对于大小超过了一个Region的大对象,将由若干个连续的Humongous Region存储,G1的大多数行为都将Humongous Region作为老年代的一部分看待。每个Region的大小可以通过XX:G1HeapRegionSize设定,取值范围1MB-32MB,且应为2的N次幂。

虽然G1仍遵循分代收集理论,即保存了新生代和老年代的概念,但事实上由于使用了Region,新生代和老年代的位置和大小已不再固定,它们都是一系列Region的动态集合。

G1的垃圾收集思路

在G1的垃圾收集过程中,G1将Region作为单次回收的最小内存单元,每次收集到的内存空间都是Region大小的整数倍,这使得G1收集器可以建立停顿时间模型(即在长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不应超过N毫秒),可以有效的避免在整个Java堆中进行全区域垃圾收集,即不具体的针对某一代进行垃圾收集。

更具体思路是让G1收集器去跟踪各个Region里面垃圾回收的价值大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,之后每次垃圾收集都根据这个优先级列表以及用户设定的允许收集停顿时间(使用-XX:MaxGCPauseMillis指定,默认200ms),优先处理回收价值收益最大的Region。这种使用Region划分空间以及具有优先级的区域回收方式,保证了G1收集器在垃圾收集过程中,可以在有限的时间内获取尽可能高的收集效率。

G1的部分实现与缺陷

1)跨Region引用问题:前几篇博客中已经提及,出现跨代引用时,为了防止将整个老年代加入GC Roots,引入了记忆集的概念来解决这个问题,但在G1收集器上记忆集的实现要更加复杂且占用资源更多。一个重要原因就是每一个Region都需要维护字节的记忆集,Region中的记忆集会记录下其他Region中指向字节的指针,同时标记这些指针分别在哪些卡页中,这种双向卡表结构(记录我指向谁的同时记录谁指向我)比原来的卡表实现更加复杂,综上两点原因(每个Region都有记忆集,双向卡表),G1收集器要比其他传统垃圾收集器占用更多内存。根据经验,G1大约要耗费Java堆的10%至20%来维持收集器工作。

2)对象消失问题:此部分面临的问题与CMS收集器相似,由于G1的收集方式和CMS相同也是并发式,那么就必须解决垃圾收集是用户线程同时修改引用的问题,CMS采取增量更新,而G1采取原始快照(SATB)算法实现。

3)浮动垃圾的产生:此外由于垃圾收集时用户线程也在与运行,那就势必会有新对象申请内存空间,G1维每个Region设计了两个名为TAMS(Top at Mark Start)的指针,将Region中的一部分空间划出来用于为新对象分配,并发回收时新分配的对象地址必须要在这两个指针的位置之上,且默认存活,不纳入本次回收范围。

4)并发失败的处理:如果垃圾收集过程中的回收速度赶不上内存分配速度,G1收集器和CMS一样也要冻结用户线程,进行一次Full GC。

5)停顿预测模型的实现:G1收集器的停顿预测模型是以衰减均值为理论基础实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集中的脏卡数量等各个可测量的步骤花费成本,并分析出平均值、标准偏差、置信度等统计信息。使用衰减均值比普通的平均值更为合适,平均值代表整体的平均状态,而衰减平均值代表最近的平均状态,能更好的分析出当前每个Region的回收价值。

G1的垃圾收集过程

G1的垃圾收集过程大致可分为以下四步

  1. 初始标记:仅标记GC Roots能够直接关联到的对象,并修改TAMS指针的值。此阶段需要暂停用户线程,停顿时间短。
  2. 并发标记:从GC Roots开始堆堆中对象进行可达性分析,递归扫描整个堆里的对象图,此阶段耗时较长,但是是并发执行。对象图扫描完后,还要重新处理原始快照记录下的在并发时有引用变动的对象。
  3. 最终标记:处理并发阶段结束后仍遗留下来的少量原始快照记录,此阶段需要短暂的暂停用户线程。
  4. 筛选回收:负责更新Region的统计数据,并根据各个Region的回收价值和成本进行综合排序,根据用户设定的允许停顿时间来制定回收计划,然后自由的选择任意多个Region构成本次垃圾收集的回收集,然后将回收集中各Region内存活的对象复制到空的Region内,然后清理掉整个就Region1的全部空间。此阶段涉及到对象移动,需要暂停用户线程,此部分由多条收集器并行完成。

从上述收集过程可见,G1收集器并没有像CMS一样选择标记-清除算法进行垃圾收集,因为G1并非单纯的追求地停顿,官方为其设定的目标是在延迟可控的情况下获取尽可能高的吞吐量。G1收集器执行过程示意图如下
在这里插入图片描述

CMS收集器与G1收集器的对比

G1明显的优点

  • 可以指定用户线程最大暂停时间。
  • 根据停顿预测模型按收益动态确定回收集,实现优先时间内达到尽可能高的收集效率。
  • 不存在空间碎片化问题,G1从局部上看使用了标记-赋值算法,总体上看是标记-整理算法,垃圾收集完成后可提供规整可用的内存,有利于应用程序长时间运行。
  • 使用原始快照解决对象消失问题可用减少并发标记和重新标记阶段的消耗,可用避免CMS在最终标记阶段停顿时间过长的缺点(但也带来了由于跟踪引用变化而产生的额外的负担)。

CMS明显的优点

  • CMS在垃圾收集过程中的内存占用和程序运行中的额外执行负载都比CMS要低。
  • 由于使用了标记-清除算法,大多场景下的用户线程停顿时间要比G1短。
  • 维护卡表时的写屏障操作更为简单(G1除了需要写后屏障维护卡表,还需要写前屏障来跟踪并发时的指针变化情况)

适用场景

小内存应用上CMS的表现大概率优于G1,大内存上G1能发挥其更多优势,优劣势的Java堆平衡点通常在6GB至8GB(书中经验之谈)。

经典垃圾收集器简单总结

在这里插入图片描述
选择合适的垃圾处理器可以很好的提升JVM性能,没有最好的垃圾收集器,只有适用于具体场景的垃圾收集器。


参考书籍 《深入理解Java虚拟机》第三版 ——周志明
本篇内容主要用于作者自身学习总结记录,才疏学浅,如文中出现纰漏,还望指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

7rulyL1ar

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

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

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

打赏作者

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

抵扣说明:

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

余额充值