阿里二面:JVM 的三色标记算法你了解吗?

简介: 阿里二面:JVM 的三色标记算法你了解吗?

一、前言

不得不说阿里的面试还是挺有质量的,这个问题直接问到了 JVM 的底层算法实现。在说 JVM 的三色标记算法之前,我们先来说下 JVM 对于常见对象存活判定算法与垃圾收集算法。常见对象存活判定算法有引用计数算法和可达性分析算法。引用计数法会产生循环引用问题,JVM 默认是通过可达性分析算法来判断对象是否存活的。而那些垃圾收集算法:标记-清除、标记-复制、标记-整理算法以及在此基础上的分代收集算法(新生代/老年代),每代采取不同的回收算法,以提高整体的分配和回收效率。

这些垃圾收集算法首先做的都是通过可达性分析算法来判定对象是否存活,首先肯定是先进行标记,这个也是理所当然的,你不先标记找到垃圾,怎么进行垃圾回收?可达性分析算法是通过一系列的 “GC roots” 对象作为根节点搜索,如果在 “GC roots” 和一个对象之间没有可达路径,则称该对象是不可达的。

迄今为止,所有垃圾收集器在根节点枚举这一步骤时都是必须暂停用户线程的,因此毫无疑问会面临 ”Stop The World“ 的困扰。啊?啥是 ”Stop The World“,也就是我们平时说的 STW,其实就是根节点枚举过程中必须在一个能保障一致性的快照中进行,说白了就相当于持久化的快照一样,在某个时间点这个过程像被冻结了。如果根节点枚举过程中整个根节点集合对象引用关系还在变化,那垃圾回收分析的结果也不会准确,所以这就导致垃圾收集过程中必须停顿所有用户线程。

想要解决或者降低用户线程的停顿,三色标记算法就登场了。为了让大家了解为啥要有三色标记算法的存在,老周在前言这里铺了很多,希望对你看下面的内容会有所帮助,那接下来我们就进入我们的正题了。

二、 三色标记算法

2.1 基本算法

事先约定:

根据可达性分析算法,从 GC Roots 开始进行遍历访问。

  • 初始状态,所有的对象都是白色的,只有 GC Roots 是黑色的。

  • 初始标记阶段,GC Roots 标记直接关联对象置为灰色。

  • 并发标记阶段,扫描整个引用链。
  • 没有子节点的话,将本节点变为黑色。
  • 有子节点的话,则当前节点变为黑色,子节点变为灰色。

  • 重复并发标记阶段,直至灰色对象没有其它子节点引用时结束。

  • 扫描完成
    此时黑色对象就是存活的对象,白色对象就是已消亡可回收的对象。
    即(A、D、E、F、G)可达也就是存活对象,(B、C、H)不可达可回收的对象。

2.2 三色标记算法缺陷

不知道你是否还记得我们前言说的,所有垃圾收集器在根节点枚举这一步骤时都是必须暂停用户线程的,产生 STW,这对实时性要求高的系统来说,这种需要长时间挂起用户线程是不可接受的。想要解决或者降低用户线程的停顿的问题,我们才引入了三色标记算法。

三色标记算法也存在缺陷,在并发标记阶段的时候,因为用户线程与 GC 线程同时运行,有可能会产生多标或者漏标。

2.3 多标

假设已经遍历到 E(变为灰色了),此时应用执行了 objD.fieldE = null (D > E 的引用断开) 。

D > E 的引用断开之后,E、F、G 三个对象不可达,应该要被回收的。然而因为 E 已经变为灰色了,其仍会被当作存活对象继续遍历下去。最终的结果是:这部分对象仍会被标记为存活,即本轮 GC 不会回收这部分内存。

这部分本应该回收但是没有回收到的内存,被称之为浮动垃圾。浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除。

另外,针对并发标记开始后的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能会变为垃圾,这也算是浮动垃圾的一部分。

2.4 漏标

假设 GC 线程已经遍历到 E(变为灰色了),此时应用线程先执行了:

var G = objE.fieldG; 
objE.fieldG = null;  // 灰色E 断开引用 白色G 
objD.fieldG = G;  // 黑色D 引用 白色G

此时切回到 GC 线程,因为 E 已经没有对 G 的引用了,所以不会将 G 置为灰色;尽管因为 D 重新引用了 G,但因为 D 已经是黑色了,不会再重新做遍历处理。

最终导致的结果是:G 会一直是白色,最后被当作垃圾进行清除。这直接影响到了应用程序的正确性,是不可接受的。

不难分析,漏标只有同时满足以下两个条件时才会发生:

  • 一个或者多个黑色对象重新引用了白色对象;即黑色对象成员变量增加了新的引用。
  • 灰色对象断开了白色对象的引用(直接或间接的引用);即灰色对象原来成员变量的引用发生了变化。

如下代码:

var G = objE.fieldG; // 1.读
objE.fieldG = null;  // 2.写
objD.fieldG = G;     // 3.写

我们只需在上面三个步骤中任意一个中,将对象 G 记录起来,然后作为灰色对象再进行遍历即可。比如放到一个特定的集合,等初始的 GC Roots 遍历完(并发标记),该集合的对象遍历即可(重新标记)。

重新标记是需要 STW 的,因为应用程序一直在跑的话,该集合可能会一直增加新的对象,导致永远都跑不完。当然,并发标记期间也可以将该集合中的大部分先跑了,从而缩短重新标记 STW 的时间,这个是优化问题了。看到了没?三色标记算法也并不能完全解决 STW 的问题,只能尽可能缩短 STW 的时间,尽可能达到停顿时间最少。

三、读屏障与写屏障

针对于漏标问题,JVM 团队采用了读屏障与写屏障的方案。

读屏障是拦截第一步;而写屏障用于拦截第二和第三步。

它们拦截的目的很简单:就是在读写前后,将对象 G 给记录下来。

3.1 读屏障

oop oop_field_load(oop* field) {
    pre_load_barrier(field); // 读屏障-读取前操作
    return *field;
}

读屏障是直接针对第一步:var G = objE.fieldG;,当读取成员变量之前,先记录下来。

void pre_load_barrier(oop* field, oop old_value) {  
    if ($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
        oop old_value = *field;
        remark_set.add(old_value); // 记录读取到的对象
    }
}

这种做法是保守的,但也是安全的。因为条件一中【一个或者多个黑色对象重新引用了白色对象】,重新引用的前提是:得获取到该白色对象,此时已经读屏障就发挥作用了。

3.2 写屏障

我们再来看下第二、三步的写操作,给某个对象的成员变量赋值时,底层代码:

/**
* @param field 某对象的成员变量,如 E.fieldG
* @param new_value 新值,如 null
*/
void oop_field_store(oop* field, oop new_value) { 
    *field = new_value; // 赋值操作
}

所谓的写屏障,其实就是指给某个对象的成员变量赋值操作前后,加入一些处理(类似 Spring AOP 的概念)。

void oop_field_store(oop* field, oop new_value) {  
    pre_write_barrier(field); // 写屏障-写前操作
    *field = new_value; 
    post_write_barrier(field, value); // 写屏障-写后操作
}

四、增量更新(Incremental Update)与原始快照(Snapshot At The Beginning,SATB)

4.1  增量更新

当对象 D 的成员变量的引用发生变化时(objD.fieldG = G;),我们可以利用写屏障,将 D 新的成员变量引用对象 G 记录下来:

void post_write_barrier(oop* field, oop new_value) {  
    if ($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
        remark_set.add(new_value); // 记录新引用的对象
    }
}

这种做法的思路是:不要求保留原始快照,而是针对新增的引用,将其记录下来等待遍历,即增量更新(Incremental Update)

增量更新破坏了漏标的条件一:【 一个或者多个黑色对象重新引用了白色对象】,从而保证了不会漏标。

4.2 原始快照

当对象 E 的成员变量的引用发生变化时(objE.fieldG = null;),我们可以利用写屏障,将 E 原来成员变量的引用对象 G 记录下来:

void pre_write_barrier(oop* field) {
    oop old_value = *field; // 获取旧值
    remark_set.add(old_value); // 记录 原来的引用对象
}

当原来成员变量的引用发生变化之前,记录下原来的引用对象。

这种做法的思路是:尝试保留开始时的对象图,即原始快照(Snapshot At The Beginning,SATB),当某个时刻的 GC Roots 确定后,当时的对象图就已经确定了。

比如当时 E 是引用着 G 的,那后续的标记也应该是按照这个时刻的对象图走(E 引用着 G)。如果期间发生变化,则可以记录起来,保证标记依然按照原本的视图来。

SATB 破坏了漏标的条件二:【灰色对象断开了白色对象的引用(直接或间接的引用)】,从而保证了不会漏标。

五、总结

基于可达性分析的 GC 算法,标记过程几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同,比如标记的方式有栈、队列、多色指针等。

对于读写屏障,以 Java HotSpot VM 为例,其并发标记时对漏标的处理方案如下:

  • CMS:写屏障 + 增量更新
  • G1、Shenandoah:写屏障 + 原始快照
  • ZGC:读屏障

上面的的方案为啥是这样的,你有想过为什么吗?

  • 原始快照相对增量更新来说效率更高(当然原始快照可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象。
  • 而 CMS 对增量引用的根对象会做深度扫描,G1 因为很多对象都位于不同的 region,CMS 就一块老年代区域,重新深度扫描对象的话 G1 的代价会比 CMS 高,所以 G1 选择原始快照不深度扫描对象,只是简单标记,等到下一轮 GC 再深度扫描。
  • 而 ZGC 有一个标志性的设计是它采用的染色指针技术,染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,如果讲这些信息直接维护在指针中,显然可以省去一些专门的记录操作。而 ZGC 没有使用写屏障,只使用了写屏障,显然对性能大有裨益的。

好了,这道面试题就说到这里,相信你跟着老周的文章看下来,心里有了自己想要的答案,我们下期再见。



欢迎大家关注我的公众号【老周聊架构】,Java后端主流技术栈的原理、源码分析、架构以及各种互联网高并发、高性能、高可用的解决方案。


喜欢的话,点赞、再看、分享三连。

相关文章
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
79 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
26天前
|
算法 Java
JVM有哪些垃圾回收算法?
(1)标记清除算法: 标记不需要回收的对象,然后清除没有标记的对象,会造成许多内存碎片。 (2)复制算法: 将内存分为两块,只使用一块,进行垃圾回收时,先将存活的对象复制到另一块区域,然后清空之前的区域。用在新生代 (3)标记整理算法: 与标记清除算法类似,但是在标记之后,将存活对象向一端移动,然后清除边界外的垃圾对象。用在老年代
22 0
|
2月前
|
算法 Java
JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
本文详细介绍了JVM中的GC算法,包括年轻代的复制算法和老年代的标记-整理算法。复制算法适用于年轻代,因其高效且能避免内存碎片;标记-整理算法则用于老年代,虽然效率较低,但能有效解决内存碎片问题。文章还解释了这两种算法的具体过程及其优缺点,并简要提及了其他GC算法。
 JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
|
2月前
|
存储 算法 Java
【JVM】垃圾释放方式:标记-清除、复制算法、标记-整理、分代回收
【JVM】垃圾释放方式:标记-清除、复制算法、标记-整理、分代回收
66 2
|
2月前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
4月前
|
存储 算法 Java
JVM自动内存管理之垃圾收集算法
文章概述了JVM内存管理和垃圾收集的基本概念,提供一个关于JVM内存管理和垃圾收集的基础理解框架。
JVM自动内存管理之垃圾收集算法
|
4月前
|
算法 Java 应用服务中间件
探索JVM垃圾回收算法:选择适合你应用的最佳GC策略
探索JVM垃圾回收算法:选择适合你应用的最佳GC策略
|
17天前
|
算法
基于WOA算法的SVDD参数寻优matlab仿真
该程序利用鲸鱼优化算法(WOA)对支持向量数据描述(SVDD)模型的参数进行优化,以提高数据分类的准确性。通过MATLAB2022A实现,展示了不同信噪比(SNR)下模型的分类误差。WOA通过模拟鲸鱼捕食行为,动态调整SVDD参数,如惩罚因子C和核函数参数γ,以寻找最优参数组合,增强模型的鲁棒性和泛化能力。
|
23天前
|
机器学习/深度学习 算法 Serverless
基于WOA-SVM的乳腺癌数据分类识别算法matlab仿真,对比BP神经网络和SVM
本项目利用鲸鱼优化算法(WOA)优化支持向量机(SVM)参数,针对乳腺癌早期诊断问题,通过MATLAB 2022a实现。核心代码包括参数初始化、目标函数计算、位置更新等步骤,并附有详细中文注释及操作视频。实验结果显示,WOA-SVM在提高分类精度和泛化能力方面表现出色,为乳腺癌的早期诊断提供了有效的技术支持。
|
3天前
|
供应链 算法 调度
排队算法的matlab仿真,带GUI界面
该程序使用MATLAB 2022A版本实现排队算法的仿真,并带有GUI界面。程序支持单队列单服务台、单队列多服务台和多队列多服务台三种排队方式。核心函数`func_mms2`通过模拟到达时间和服务时间,计算阻塞率和利用率。排队论研究系统中顾客和服务台的交互行为,广泛应用于通信网络、生产调度和服务行业等领域,旨在优化系统性能,减少等待时间,提高资源利用率。
下一篇
DataWorks