聊聊CMS、G1垃圾回收器

大家都知道我们使用Java进行程序开发时,编码过程中是不需要像C++手动管理内存。Java这种特性大大降低了程序开发的难度,这也可以说是Java语言流行这么多年的一个原因吧。但内存不是无限的,程序开发者没有进行内存管理那必然是有人帮我们做了(所谓的岁月静好只不过是有人在替我们负重前行),它就是Java虚拟机的垃圾回收器。

公司项目因为业务场景需求,接口在保证并发量的同时又得控制JVM最大停顿时间,垃圾回收器从java8默认的Parallel Old改成CMS,最终调整为G1,再加上参数优化,才比较好的保证了业务需求。

今天这篇文章我们只聊jdk8中的CMS和G1回收器,这也是实际互联网项目中用的比较多的两种垃圾回收器。

1.CMS收集器

在Java 8中,CMS(Concurrent Mark-Sweep)是一种老式的垃圾回收器。它主要针对旨在减少应用程序停顿时间的需求而设计。CMS垃圾回收器旨在降低应用程序的停顿时间,特别是针对大型堆内存而言,因为对于大型堆内存,使用传统的垃圾回收器可能导致长时间的停顿。

从名字(包含"Mark Sweep")上就可以看出CMS收集器是基于标记-清除算法实现的,它的运作 过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:

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

从图表中可以清楚地看出,在CMS(Concurrent Mark-Sweep)的垃圾回收过程中,初始标记和重新标记这两个阶段会导致STW(Stop-The-World)情况,即用户线程会被暂停。不过,初始标记阶段仅仅标记了与GC Roots相关联的对象,因此执行速度较快。并发标记阶段负责进行GC Roots追踪,而重新标记阶段则是为了修正在并发标记期间由于用户线程继续运行而导致标记状态发生变化的那部分对象。相较于初始标记阶段,重新标记阶段的停顿时间通常稍长,但远远短于并发标记阶段。

然而,CMS收集器存在一些不足之处:

  • CMS对CPU资源非常敏感。由于一部分线程用于垃圾回收,因此可能导致吞吐量下降。CMS默认启动的回收线程数为(CPU数量+3)/ 4,如果CPU数量较少,则吞吐量下降显著。
  • CMS无法处理浮动垃圾(Floating Garbage),由于并发清理阶段用户线程仍在运行,新的垃圾会不断产生,这部分垃圾只能在下一次GC时清理。这也意味着CMS不能像其他收集器一样等老年代满了再启动,需要提前预留足够的空间,否则可能导致Concurrent Mode Failure,导致另一次 Full GC 的产生。
  • CMS采用标记清除算法,可能导致大量内存碎片,影响大内存分配。当无法找到足够大的连续空间来分配对象时,将会触发Full GC,从而影响应用性能。可以通过开启选项-XX:+UseCMSCompactAtFullCollection来在CMS无法处理时执行内存整理,但这会增加停顿时间,并且可以通过另一个参数-XX:CMSFullGCsBeforeCompation设置执行多少次不压缩的Full GC后跟随一次带压缩的Full GC。

2.Garbage First收集器

Garbage-First(G1)收集器是Java虚拟机(JVM)提供的一种新型垃圾收集器,从Java 7开始引入,并在Java 9后逐渐成为默认的垃圾回收器。

G1 收集器的工作步骤如下:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

可以看到整体过程与 CMS 收集器非常类似,筛选阶段会根据各个 Region 的回收价值和成本进行排序,根据用户期望的 GC 停顿时间来制定回收计划。

  1. 分代收集器转型: 虽然G1也是分代垃圾收集器,但它不再像传统的分代收集器(如CMS)那样严格区分新生代和老年代。它将整个堆划分为多个大小相等的区域,这些区域既可以是新生代对象,也可以是老年代对象。这有助于G1更加灵活地管理内存,减少内存碎片化。
  2. 目标停顿时间: G1收集器采用"Garbage-First"的策略,它的设计目标是在可控制的停顿时间内(与用户指定的目标停顿时间相关),尽量实现高效的垃圾回收。相比于CMS等垃圾收集器,G1更加注重在有限的时间内达到更稳定的停顿表现。
  3. 并发和增量清理: G1收集器采用并发和增量的方式来执行大部分垃圾收集工作,尽量减少全局性的停顿。这意味着G1在进行垃圾回收的同时,可以让部分用户线程继续执行。
  4. 区域化管理: G1将堆内存划分为多个小区域(Region),并使用复制算法来收集垃圾。这种区域化的管理有利于更有效地执行垃圾回收,同时也有助于避免全堆扫描。
  5. 混合回收: G1收集器同时包含了全局垃圾回收和部分区域垃圾回收的概念。通过选择若干个区域进行混合回收,G1能够在一定程度上保证停顿时间的可控性,并且能够逐步实现更加高效的全局垃圾回收。
  6. 内存整理和压缩: G1会定期进行内存整理和压缩操作,以减少碎片和提高内存使用率。这有助于避免过多的碎片化,从而降低Full GC的频率。

总体而言,G1收集器相比传统的CMS收集器在可预测性、内存整理、停顿时间方面表现更好,特别是在大堆内存和需要低延迟的应用场景下具有更大优势。通过区域化管理和目标停顿时间的设定,G1收集器能够更有效地管理内存和执行垃圾回收,提供更稳定且可预测的性能表现。

目前在小内存应用上CM S的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其 优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间---《深入理解Java虚拟机》周志明

三色标记:

无论是CMS,还是G1垃圾回收器,都是使用的可达性分析算法来判定对象是否存活,这两种垃圾回收器为了尽可能的降低STW时间,都有使用并发标记,而我们都容易想到,用户线程在并发执行中时,对象是否为垃圾是不确定的,这无疑会产生多标、漏标这两种情况,这该如何解决呢?

我们引入三色标记(Tri-color Marking)[1]作为工具来辅助推导,把遍历对象图过程中遇到的对象,按照"是否访问过"这个条件标记成以下三种颜色:

  • 白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。

  • 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代 表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对 象不可能直接(不经过灰色对象)指向某个白色对象。

  • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。

关于可达性分析的扫描过程,我们发挥一下想象力,把它看作对象图上一股以灰色为波峰的波纹从黑向白推进的过程,如果用户线程此时是冻结的,只有收集器线程在工作,那不会有任何问题。但如果用户线程与收集器是并发工作呢?收集器在对象图上标记颜色,同时用户线程在修改引用 关系------即修改对象图的结构,这样可能出现两种后果。一种是把原本消亡的对象错误标记为存活, 这不是好事,但其实是可以容忍的,只不过产生了一点逃过本次收集的浮动垃圾而已,下次收集清理 掉就好。另一种是把原本存活的对象错误标记为已消亡,这就是非常致命的后果了,程序肯定会因此 发生错误,下面表3-1演示了这样的致命错误具体是如何产生的。

Wilson于1994年在理论上证明了,当且仅当以下两个条件同时满足时,会产生"对象消失"的问 题,即原本应该是黑色的对象被误标为白色:

·赋值器插入了一条或多条从黑色对象到白色对象的新引用;

·赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

因此,我们要解决并发扫描时的对象消失问题,只需破坏这两个条件的任意一个即可。由此分别 产生了两种解决方案:增量更新(CMS采用的方法)和原始快照(G1采用的方法)。

  • 增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新 插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫 描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。

  • 原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删 除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描 一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来 进行搜索。

参考资料:《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》

相关推荐
Adolf_19938 分钟前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
Jarlen8 分钟前
将本地离线Jar包上传到Maven远程私库上,供项目编译使用
java·maven·jar
蓑 羽14 分钟前
力扣438 找到字符串中所有字母异位词 Java版本
java·算法·leetcode
叫我:松哥20 分钟前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
Reese_Cool21 分钟前
【C语言二级考试】循环结构设计
android·java·c语言·开发语言
海里真的有鱼22 分钟前
Spring Boot 项目中整合 RabbitMQ,使用死信队列(Dead Letter Exchange, DLX)实现延迟队列功能
开发语言·后端·rabbitmq
工业甲酰苯胺33 分钟前
Spring Boot 整合 MyBatis 的详细步骤(两种方式)
spring boot·后端·mybatis
严文文-Chris1 小时前
【设计模式-享元】
android·java·设计模式
Flying_Fish_roe1 小时前
浏览器的内存回收机制&监控内存泄漏
java·前端·ecmascript·es6