JVM之CMS、G1|ZGC详解以及选型对比
CMS
cms是第一款实现用户线程和垃圾回收线程并行,实现垃圾回收过程中用户线程依然能正常运行的垃圾回收器
整体步骤分为以下4步
- 初始标记 这里是 stw的,用来记录根对象
- 并发标记 和用户线程并行,记录和根对象直接或间接相连的全部对象,是一个闭包结构
- 重新标记 因为上一步是并发,用户线程也在操作会产生新的对象,因此这里要停止用户线程进行并发重标记
- 并发清除 跟用户线程并发进行垃圾对象的清理
jdk9之后已标记为过时不再进行使用
G1
G1对比CMS,最核心的能力就是 可预测停顿 和 高吞吐量
G1的内存不再严格遵守老年代和新时代的概念,而是统一以 Region 作为单位。追踪 Region 并创建Region的优先级列表,在合适的时机进行回收。G1同时回收新生代和老年代,称为G1的young gc 和 mixed gc模式
Region结构
主要分为两部分,一部分是 Card Table,一部分是Rset记忆集合
Card Table 是 Region 的内部结构划分,一个 Region 分为9块 card
Rset是一个hash表,key是引用这个region的其他region起始地址,value 是被key对应的region引用的card索引位置。这种设计是为了解决跨代引用。新生代对象被老年代对象引用,为了通过引用链来找到这个新生代对象,就要经过老年代,实际上就是把全部对象扫描了一遍,因此这里通过hash表结构记录位置实现跳过。
young gc
整个young gc都是处在stw的,因此要么并发gc要么减少新生代的region数
扫描根对象,如果指向老年代就停止,新生代就继续向下向深扫描 -》 排空dirty card quene,更新Rset,记录哪些对象被老年代进行跨代引用 -》 扫描Rset -》拷贝对象到survivor区 / 晋升老年代 -》 处理引用队列
老年代引用新生代时,引用记录对应的card会塞入线程私有的一个队列中,如果这个队列满了就会转移到全局的队列中,这个操作是为了解决直接更新Rset导致多线程竞争过于频繁的问题。根据这个队列的容量有不同的操作,中等容量就启动部分线程,中上就启动全部线程,快满了就启动应用线程一起,来拖慢ditry card的产生速度,
三色标记
三色标记是可达性分析算法是具体实现,三色分为白、黑、灰,各自含义如下
具体步骤就不说了,类似岛屿问题中的查找岛屿数量。或者橘子腐败问题
但是又有点不同会导致三色标记存在一些缺陷,因为这个标记过程是和用户线程并发执行的,引用之间会发生变化,导致漏标问题的出现
已存在对象漏标
小D原本是奴隶A(被检查的灰色)的孩子,如果被发现是要打去矿洞做奴隶的,小A为了小D不重复他的命运(也被检查),把小D染色得跟已确定身份的富豪B(b和b的全部孩子已被检查,黑色)一个颜色,然后说是富豪B的孩子,富豪B也认可了这个孩子,上级就不会再去查一次小D的身份(因为默认b的孩子都检查过了)于是小D成功逃脱了自己要当矿工的命运
新产生对象被漏标
跟上面一样,实际上就是富豪B在标记为黑色后突然又生了一个新儿子
通过上面的例子我们应该也知道如何进行解决这种事情的发生,简单来说就是记录,引用的变更都记录下来(打上特殊烙印,无论如何变更身份都跳不出被检查的命运)
Mixed GC
分为两部分 全局并发标记,拷贝存活对象
并发标记的步骤如下
- 初始标记 和young gc共用stw时间,减少stw
- 根分区扫描 这一步要扫描survivor区域,于是不能发生young gc 耗时短
- 并发标记
- 最终标记
- 清点垃圾 这一步把需要回收的region记录到一个集合中,再记录空对象的region留给拷贝对象存活的步骤
G1点评
从G1的设计上来看,它使用了大量的额外结构来存储引用关系,并以此来减少垃圾回收中标记的耗时。但是这种额外的结构带来的内存浪费也是存在的,极端情况甚至可以额外占用20%的内存。而基于region的内存划分规则则让内存分配更加复杂,但是这也有好处。就是内存的回收后产生的碎片更少,也就更少触发full gc。
根据经验,在大部分的大型内存(6G以上)服务器上,无论是吞吐量还是STW时间,G1的性能都是要优于CMS。
ZGC
ZGC也是把堆内存分为一系列内存分区,称为page,但是ZGC的page不具有分代的特点,page分为三种,小中大page。
核心能力是 停顿时间不受到堆内存大小影响,对大内存机器GC能力强
想要了解ZGC的并发回收需要先了解三个概念
染色指针
染色指针是一种让指针存储额外信息的技术,操作系统里,一个内存地址共有64位,但是实际上是没用满64位的,操作系统的“物理内存地址”实际上是操作系统虚拟出来的虚拟地址,也就是说,并不是内存颗粒对应的地址。是一种虚拟内存管理技术
对于JVM来说,一个对象的地址只使用前42位,43-46用来存储的是额外的信息,也就是说,如果使用ZGC,我们可以在43-46位分别标注额外信息来实现染色指针帮助GC回收
读屏障
读屏障是JVM向应用代码插入一小段代码的技术,当线程从堆中读取对象引用的时候触发,主要用来改写地址的命名空间,
NUMA
了解即可
简单例子:
想象一个大型办公室,里面有两个独立的工作区域(类似两个 CPU)。每个区域里有自己的员工(类似 CPU 核心)和专属的文件柜(类似本地内存),两个区域之间有一条走廊(类似 CPU 间的连接通道)。
当 A 区域的员工需要文件时,优先从自己区域的文件柜拿(速度快,类似本地内存访问);
如果文件在 B 区域的文件柜里,就需要通过走廊去取(速度慢,类似跨区域内存访问)。
这里的 “两个工作区域 + 各自文件柜 + 走廊” 的结构,就类似 NUMA(非统一内存访问)架构:CPU 访问本地内存的速度远快于访问其他 CPU 的内存。
ZGC流程
- 标记 从跟开始标记存活对象
- 转移 把部分活跃对象转到新内存空间
- 重定位,对象地址变了,引用的指针也要相应换到新对象地址,这里运用了读屏障
CMS和G1都使用写屏障,在对对象引用赋值的地方使用AOP,读屏障是在读取引用的地方使用AOP,在指针的43-46位做特殊标记实现额外信息记录,应用程序根据这来片段对象是不是转移并帮忙处理部分转移对象的工作
ZGC和G1对比
对比起G1来说有以下劣势
- 承受的对象分配速率不高,如果对象分配速率很高,会导致大量对象被当成存活对象,出现大量浮动垃圾。
- 吞吐量小于G1,因为占用了应用线程来帮助处理,而且把stw拆碎到更大时间范围和用户线程并行,不可避免减弱了业务线程的处理能力
优势如下
对比起G1来说,节省了内存空间,因为使用指针记录信息,G1的region使用Rset来记录信息实现跨代引用
一旦某个Region存活对象被转移走就立刻回收和释放,比G1全部转移完再处理的方式来说,更高效,只要有1个空闲region也能完成收集
参考文章如下
极致八股文之JVM垃圾回收器G1&ZGC详解
新一代垃圾回收器ZGC的探索与实践