GC 三色标记算法(Go Java版本)

news2024/10/7 18:28:37

一、前言

GC全称Garbage Collection,目前主流的垃圾回收算法有两类,分别是追踪式垃圾回收算法(Tracing garbage collection)和引用计数法( Reference counting )。

而三色标记法是属于追踪式垃圾回收算法的一种。追踪式算法的核心思想是判断一个对象是否可达,因为一旦这个对象不可达就可以立刻被 GC 回收了。

那如何判断一个对象是否可达呢?

在go推出三色标记法之前,Go 所使用的gc算法叫Mark-And-Sweep(标记清除)。这个算法就是严格按照追踪式算法的思路来实现的。

1.1 标记清除算法

标记清除(Mark-Sweep)算法是最常见的垃圾收集算法,标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段:

  • 标记阶段 — 从根对象出发查找并标记堆中所有存活的对象;
  • 清除阶段 — 遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表;

如下图所示,内存空间中包含多个对象,我们从根对象出发依次遍历对象的子对象并将从根节点可达的对象都标记成存活状态,即 A、C 和 D 三个对象,剩余的 B、E 和 F 三个对象因为从根节点不可达,所以会被当做垃圾:
在这里插入图片描述
标记阶段结束后会进入清除阶段,在该阶段中收集器会依次遍历堆中的所有对象,释放其中没有被标记的 B、E 和 F 三个对象并将新的空闲内存空间以链表的结构串联起来,方便内存分配器的使用。在这里插入图片描述
这里介绍的是最传统的标记清除算法,垃圾收集器从垃圾收集的根对象出发,递归遍历这些对象指向的子对象并将所有可达的对象标记成存活;标记阶段结束后,垃圾收集器会依次遍历堆中的对象并清除其中的垃圾,整个过程需要标记对象的存活状态,用户程序在垃圾收集的过程中也不能执行,我们需要用到更复杂的机制(三色标记法)来解决 STW 的问题。

1.2 垃圾收集的执行周期

CMS 收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:

1)初始标记(CMS initial mark) 暂停所有的其他线程(STW)。记录下 GC ROOT 直接引用对象,速度很快。

2)并发标记(CMS concurrent mark) 并发标记阶段就是从 GC ROOT 行的直接关联对象开始遍历整个对象的过程,这个过程耗时比较长但是不需要停顿用户线程,可以与垃圾收集器一起并发运行。因此用户程序继续运行,可能会导致已经标记过的对象状态发生变化。

3)重新标记(CMS remark) 重新标记阶段就是为了修正并发标记期间,因为用户程序继续运行而导致标记产生变动,的那一部分对象的标记记录。这个阶段的停顿时间一般比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要是用到三色标记里的增量更新算法

4)并发清除(CMS concurrent sweep)开启用户线程,同时 GC 线程开始对未标记的区域做清扫,这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。

三色标记法是传统 Mark-Sweep 的一个改进,它是一个并发的 GC 算法。on-the-fly

二、什么是三色标记法

为了解决原始标记清除算法带来的长时间 STW,多数现代的追踪式垃圾收集器都会实现三色标记算法的变种以缩短 STW 的时间。

主流的垃圾收集器基本上都是基于可达性分析算法来判定对象是否存活的。根据对象是否被垃圾收集器扫描过而用白、灰、黑三种颜色来标记对象的状态的一种方法。而其中

  • 白色对象 —表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始阶段,所有的对象都是白色的,若在分析结束之后对象仍然为白色,则表示这些对象为不可达对象,对这些对象进行回收。
  • 黑色对象 —表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经被扫描过。黑色表示这个对象扫描之后依然存活,是可达性对象,如果有其他对象引用指向了黑色对象,无须重新扫描,黑色对象不可能不经过灰色对象直接指向某个白色对象。
  • 灰色对象 — 表示对象已经被垃圾收集器访问过,但是这个对象至少存在一个引用(属性)还没有被扫描过。
    在这里插入图片描述

三、三色标记的过程

从我们main方法的根对象(JVM中称为GC Root)开始沿着他们的对象向下查找,用黑灰白的规则,标记出所有跟GC Root相连接的对象(不递归),扫描一遍结束后,一般需要进行一次短暂的STW(Stop The World)。

只需找出灰色对象并顺着继续往下标记(且因为大部分的标记工作已经在第一次并发的时候发生了,所以灰色对象数量会很少,标记时间也会短很多), 此时程序继续执行,GC线程扫描所有的内存,找出扫描之后依旧被标记为白色的对象(垃圾)并清除。

3.1 标记的原理

整个进程空间里申请每个对象占据的内存可以视为一个图, 初始状态下每个内存对象都是白色标记。

第一轮:先stop the world,将扫描任务作为多个并发的goroutine立即入队给调度器,进而被CPU处理,第一轮先扫描所有可达的内存对象,标记为灰色放入队列。

第二轮:可以恢复start the world,将第一步队列中引用的对象置为灰色加入队列,一个对象引用的所有对象都置灰并加入队列后,这个对象才能置为黑色并从队列之中取出。循环往复,最后队列为空时,整个图剩下的白色内存空间即不可到达的对象,即没有被引用的对象;

第三轮:再次stop the world,将第二轮过程中新增对象申请的内存进行标记(灰色),这里使用了writebarrier(写屏障)去记录这些内存的身份;

这个算法可以实现 on-the-fly(即时),也就是在程序执行的同时进行收集,并不需要暂停整个程序。

标记的过程

3.2 标记的步骤

(1)首先创建三个集合:白、灰、黑。
在这里插入图片描述
(2)将所有对象放入白色集合中。
在这里插入图片描述
(3)然后从根节点开始遍历所有对象(注意这里并不递归遍历),把遍历到的对象从白色集合放入灰色集合。因为root set 指向了A、F,所以从根结点开始遍历的是A、F,所以是把A、F放到灰色集合中。
在这里插入图片描述
(4)之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合,我们可以发现这个A指向了B,C,D所以也就是把BCD放到灰色中,把A放到黑色中,而F没有指任何的对象,所以直接放到黑色中。
在这里插入图片描述
(5)重复 4 直到灰色中无任何对象,因为D指向了A所以D也放到了黑色中,而B和C能放到黑色集合中的道理和F一样,已经没有了可指向的对象了。
在这里插入图片描述
(6)通过write-barrier检测对象有无变化,重复以上操作,由于这个EGH并没有和RootSet有直接或是间接的关系,所以就会被清除。
在这里插入图片描述
(7)收集所有白色对象(垃圾)
在这里插入图片描述
所以我们可以看出这里的情况,只要是和root set根集合直接相关的对象或是间接相关的对象都不会被清楚。只有不相关的才会被回收。

四、标记存在的问题—多标和漏标

垃圾收集器在并发标记的过程中,执行标记期间应用线程还在并行运行,对象间的引用关系时刻发生变化,垃圾收集器在标记过程中就容易发生多标和漏标(其实多标和漏标我们统称为误标)。

三色标记示例

public class TriColorMarking {

    public static void main(String[] args) {
        A a = new A();
        //开始做并发标记
        D d = a.b.d;   // 1.读
        a.b.d = null;  // 2.写
        a.d = d;       // 3.写
    }
}

class A {
    B b = new B();
    D d = null;
}

class B {
    C c = new C();
    D d = new D();
}

class C {
}

class D {
}

例子的一个简单说明:

  1. 在 new A() 的时候会创建引用关系 A -> B ,B-> C , B -> D;
  2. 当我们做并发标记的时候,垃圾收集器访问过 A、B、C、D 最终都标记为黑色。但是这个时候程序执行了一个 a.b.d = null 就标识 D 其实是没有引用,理论上 D 对象可以被回收。这种情况就产生了 “浮动垃圾”。
  3. 当我们发现了 D 没有引用,标记为白色,但是在标记完成过后发现 a.d = d 。又新增了对象引用如果将 d 回收掉程序就会报错肯定是不行的。这是一个典型的 “多标” 场景。

下面我们会通过并发标记的过程中出现的漏标和多标场景进行分析。

4.1 漏标

在并发标记过程中,将原本消亡的对象标记为存活对象,这就是漏标。就会产生浮动垃圾,需要等到下次 GC 的时候清理。

产生过程:程序删除了全部从灰色对象到该白色对象的直接或者间接引用。
在这里插入图片描述
其实浮动垃圾是可以接受的只会影响垃圾收集器的效率,或者说是收集的比率。

4.2 多标

在并发标记过程中,将原本存活的对象标记为需要回收的对象。

产生过程:程序插入一条或者多条从黑色对象到白色对象的新引用。
在这里插入图片描述
这种情况是不可以接受的,如果正在被使用的程序对象被 JVM 回收,会导致程序运行错误,是不可以接受的会导致严重 BUG。

五、解决漏标和多标的方案

解决漏标和多标分别有两种解决方案:增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning, STAB)。

  • 增量更新(Incremental Update):在并发标记过程中,当黑色对象插入了新的指向白色引用关系时,就将这个插入引用记录下来,并发标记结束后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。简化理解, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变成灰色对象
  • 原始快照(Snapshot At The Beginning, STAB):这并发标记过程中,当灰色对象要删除白色对象的引用关系时,就将这个需要删除的记录下来,在并发扫描结束后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次,这样就能扫描到白色对象,将白色的对象直接标记为黑色(目的就是为了让这种对象在本轮 GC 清理中能够存活下来,待下一轮 GC 的时候重新扫描,这个对象也可能成为浮动垃圾) 总之,无论是引用关系记录插入还是删除,虚拟机的记录操作都是通过写屏障来实现的。

5.1 写屏障(Write Barrier)

JVM 通过写屏障(Write Barrier)来维护卡表,卡表是记忆集的实现。记忆集是用来缩小 GC Root 的扫描范围,我们在 GC 的时候只需要去过滤卡表变脏(Dirty)的元素,找到具体一块卡页内存块,放入 GC Root 中一块扫描。这是大概的一个流程,后续会讲到,先有一个印象。再回到写屏障,下面是一个对象赋值操作:

/**
* @param field 某对象的成员变量,如 a.b.d 
* @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);  // 写后屏障
}

5.1.1 写屏障之STAB

当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d = null),我们可以利用写屏障,将B原来成员变量的引用对象D记录下来:

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

5.1.2 写屏障之增量更新

当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D记录下来。

void post_write_barrier(oop* field, oop new_value) {  
    remark_set.add(new_value);  // 记录新引用的对象
}

5.2 读屏障(Load Barrier)

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

读屏障是直接针对第一步:D d = a.b.d,当读取成员变量时,一律记录下来:

void pre_load_barrier(oop* field) {  
    oop old_value = *field;
    remark_set.add(old_value); // 记录读取到的对象
}

5.3 记忆集和卡表(Remembered Set And Card Table)

垃圾收集器在新生代建立了记忆集(Remembered Set)的数据结构,用来避免把整个老年代的 GC root 扫描一遍。事实上并不只是新生代、 老年代之间才有跨代引用的问题, 所有涉及部分区域收集(Partial GC) 行为的垃圾收集器, 典型的如G1、 ZGC 和 Shenandoah 收集器, 都会面临相同的问题。记忆集是一种记录非收集区域指向收集区域的指针集合抽象的数据结构

Hotspot 中使用一种叫做 “卡表” (Card Table)的方式来实现记忆集,也是目前最常用的一种方式。卡表和记忆集的关系,可以类比 Java 语言中 HashMap 和 Map 之间的关系。卡表是一个字节数组实现:CARD_TABLE[], 每个元素都对应着一个标识的内存区域一块特定大小的内存块,称为“卡页”。Hotsport 卡页的大小是 2^9 也就是 512 字节。
在这里插入图片描述
一个卡页中可以包含多个对象,只要卡页内一个或者多个对象的字段存在跨代引用,其对应的卡表的元素标识就变成了1,表示该元素变脏,否则为 0。GC 时,只需要筛选卡表中变脏的元素加入到 GCRoot 中。

卡表的维护
如何让卡表变脏,即发生引用字段赋值时,如何更新卡表对应的标识为 1。Hotspot使用写屏障维护卡表状态。

5.4 收集器采用的解决方案

  • CMS : 写屏障,增量更新
  • G1,Shednandoah: 写屏障 + STAB
  • ZGC:读屏障

5.4.1 为什么 G1 采用 SATB,CMS 使用增量更新?

因为SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象。

而CMS对增量更新的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描。

六、总结

垃圾收集器的实现非常复杂,我们在分析垃圾收集器的过程中不得不省略很多的实现细节,其中包括并发标记对象的过程、清扫垃圾的具体实现,这些过程设计大量底层的位操作和指针操作。

垃圾收集是一门非常古老的技术,它的执行速度和利用率很大程度上决定了程序的运行速度,其中Go 语言为了实现高性能的并发垃圾收集器,使用三色抽象、并发增量回收、混合写屏障、调度算法以及用户程序协助等机制将垃圾收集的暂停时间优化至毫秒级以下,从早期的版本看到今天,我们能体会到其中的工程设计和演进。

相关材料

  1. JVM 从入门到放弃之 Java 对象创建过程
  2. JVM 垃圾回收算法和 CMS 垃圾回收器
  3. JVM 从入门到放弃之 ZGC 垃圾收集器
  4. JVM 运行时内存分代结构
  5. JVM 字节码解析过程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/552033.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

github copilot chat申请,安装,及常见问题解决

申请 首先申请,并开通copilot, 地址为:https://github.com/features/copilot,copilot 一个月10美金,第一个月免费,支持国内的信用卡。 开通copilot之后,可以申请 copilot chat 的预览版功能,网…

5-高级SQL查询

PS:插入查询结果 把一张表查询出的结果,插入到另一张表中。要求两张表的列数和列类型要匹配。 前面讲的所有select规则在此处都适用~ 1.聚合查询 聚合查询:行和行之间进行运算。带表达式查询:列和列之间进行运算。 常⻅的聚合函…

MySQL——聚合函数

我们上一篇讲到了 SQL 单行函数。实际上 SQL 函数还有一类,叫做聚合(或聚集、分组)函数,它是对一组数据进行汇总的函数,输入的是一组数据的集合,输出的是单个值。接下来我们来看看什么是聚合函数及聚合函数的分类 1. 聚合函数介绍 1.1 什么是聚合函数? 聚合函数作用于…

redis 7.x 缓存双写一致性的解决方案

一 redis缓存双写一致性 1.1 保证redis一致性的原则 1.给缓存设置过期时间,定期清理缓存并写回,是保证最终一致性的解决方案。使用场景:在数据读多写少的情况下作为缓存来使用。 我们可以对已存入缓存的数据设置过期时间,所有…

Qt-学习 QJson协议解析

内容来源于哔站视频,仅仅作为自己的笔记记录,感兴趣的小伙伴去原作品大call(此处给作者三鞠躬),Json以及Json在Qt中的使用 【Qt/C/C】_哔哩哔哩_bilibili 目录 1、Json介绍: 1.1 Json的定义 1.2 Json的数据格式 1…

Java面试知识点(全)- Java并发- Java并发基础

Java面试知识点(全) 导航: https://nanxiang.blog.csdn.net/article/details/130640392 注:随时更新 多线程解决什么问题 CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异&#xff0c…

thinkphp6中使用session设置无效问题的解决及注意事项

就如thinkphp官方在文档开头所描述的一样,ThinkPHP6.0基于精简核心和统一用法两大原则在5.1的基础上对底层架构做了进一步的优化改进,并更加规范化;其实从5.x版本开始thinkphp本身就是为api应用而生。特别是在6.0版本开始框架默认压根不开启和…

大数据-玩转数据-FLINK(Yarn模式)的安装与部署

一、为什么要用Flink on Yarn HA 模式 默认情况下,Flink 只有一个JobManager,这将导致单点故障,使用JobManager HA ,集群可以从单点故障中恢复,从而避免单点故障,我们可以在Standalone 或 Flink on Yarn 集…

【2023 · CANN训练营第一季】初识新一代开发者套件 Atlas 200I DK A2---章节小测微认证考试

微认证考试 1、以下不属于模型适配工具内置的模型的是? A. 目标检测模型 B. 图像分类模型 C. 语义分割模型 D. 关键点检测模型 C 2、模型适配工具安装前需要先安装? A. mobaXterm B. python3.9 C. Vscode D. Anaconda D 3、在哪个目录下可查看端…

docker 部署 -v指令遇到docker: invalid reference format.

检查了半天才发现,换行符后面不能有空格!有空格就报错!

vite创建vue3项目

vite创建vue3项目 方式一(推荐) 脚手架快速创建,勾选依赖的方式 这种方式创建的项目最快捷,因为基本依赖都装好了 npm init vuelatest方式二 使用官方模板创建vue3项目 这种方式会基于模板创建项目,对于官方模板vu…

Rollup 基本概念及使用

Rollup是一款基于ESModule模块规范实现的JavaScript打包工具,在前端社区中赫赫有名,同时也在Vite的架构体系中发挥着重要作用。不仅是Vite生产环境下的打包工具,其插件机制也被Vite所兼容,可以说是Vite的构建基石。 接下来&#…

cesium entity与时间轴关联(添加运动轨迹)

1、先添加一个entity实体 用model加载一个飞机实体 let entity viewer.entities.add({position: Cesium.Cartesian3.fromDegrees(120, 21),model: {uri: "/static/Cesium_Air.glb",minimumPixelSize: 128,maximumScale: 20000,},}) viewer.trackedEntity entity;…

LIN-报文结构

文章目录 协议规范一、字节场二、报文头(HEADER FIELDS)同步间隔(synchronisation break)同步场(SYNCH FIELD)标识符场(IDENTIFIER FIELD) 三、数据场(DATE FIELDS)四、校…

【负载均衡式在线OJ】 数据库

文章目录 41.使用Postman进行综合调试42.了解-前端预备52. 添加oj用户到MySQL53. 使用MySQL_Workbench创建表结构54. 测试录题功能55.重新设计oj_model56.编写oj_model具体代码57.MySQL综合测试58.结项与项目扩展思路 41.使用Postman进行综合调试 完善判题功能 先编译再测试 …

SpingBoot简介—helloworld

目录 一、使用idea创建第一个SpringBoot项目 二、Hello World 三、整体项目结构 一、使用idea创建第一个SpringBoot项目 在进行这一步时,可能会遇到问题,如下图所示: 在这里解决方法有以下四种: (1)方法1&…

国内AI大模型 VS chatgpt (20)-- H265/H264有何不同,h265有什么优势?

H265/H264有何不同,h265有什么优势? 文心一言: H.264和H.265是两种不同的视频编码标准,它们在压缩质量和带宽需求方面有所不同。 H.264是一种高效的视频压缩标准,它的编码单位是宏块(macroblock)&#…

XML配置方式整合SSM框架实现用户登录

文章目录 一,创建数据库与表(一)创建数据库(二)创建用户表(三)在用户表里插入记录 二,创建Maven项目三,项目添加相关依赖四,创建日志属性文件五,创…

Aspose.Tasks for Java 23crack

Aspose.Tasks for Java 23crack 新属性允许您控制“分配成本”和“剩余成本”的自动计算。 Aspose.Tasks for Java是一个项目管理API,可帮助您操作Microsoft project文档。开发人员可以定义项目的主要属性和默认属性以及日历信息。他们还可以指定工作日和日历例外情…

【5.21】六、自动化测试—持续集成测试

目录 6.4 持续集成测试 6.4.1 持续集成的概念 6.4.2 持续集成测试框架设计 6.4 持续集成测试 持续集成(Continuous Integration,CI)是软件开发DevOps(DevelopmentOperations)中的一个概念,它强调的是软…