聊聊Go的三色标记法

2023-11-05 22:58
文章标签 标记 go 聊聊 三色

本文主要是介绍聊聊Go的三色标记法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

这里是Z哥的个人公众号

每周五11:45 按时送达

当然了,也会时不时加个餐~

我的第「203」篇原创敬上

大家好,我是 Z 哥。

今天带来一篇久违的技术型文章。

之前也有不少小伙伴会问,Z 哥你好久没发技术性文章了。其实主要原因有以下几点。

第一,目前的工作偏业务以及管理,的确在技术上的精力投入不如之前那么多。这也限制了自己在纯技术性方面的知识输出。

第二,虽然自己在工作之余,也会有一部分精力专门用于技术学习,但是大多是以新技术、新框架等的了解、熟悉为主。涉及到的知识 Level 相对比较浅,就算发出来对大家的帮助也不大,就没发。

第三,从长远来看,自己也不想太把自己局限在技术的圈子里。因为在我看来,技术只是一门手艺,是吃饭的家伙,但是吃饭的家伙从来都不仅仅是技术,还有很多其它的方面。甚至其中很多事情不像具体的技术细节那样「标准化」,有很多是通过血汗积累的「非标准化」经验,我认为这些经验的价值不亚于技术知识。因此,作为有志与大家交朋友的 Z 哥,自然就不想把自己局限在「技术」这个小圈子里。

好了,回到本文的正题。最近正好在学习 Golang,对它的里面用到的三色标记法的 GC 机制有些好奇(最开始是因为名字让我联想到了三色杯冷饮~),就稍微多深入了解了一下,在这里分享出来,或许将来对你面试啥的有些帮助。

/01  判断对象存活的思路/

在 GC 领域里,判断对象存活的主流思路是两个,「引用计数」和「可达性分析」。

01  引用计数

顾名思义,引用计数的思路就是给每个对象进行计数,每被其它对象引用一次,计数就 +1,引用失效后,计数就 -1。当计数器的数值为 0,就意味着它没有被使用,可以回收。

02  可达性分析

可达性分析的思路就是通过引用链路判断对象是否可被触达,如果能触达说明该对象当前正在被使用,不可回收;反之,没有触达到的对象则认为是无使用的,可以回收。

这个引用链路的结构类似于有向有环图,但是根节点不止一个,是一个集合,称之为 GCRoots。

目前主流的 GC 机制大多用的是「可达性分析」这条路线。Go、Java、.Net等都是如此。为什么引用计数不好用呢?因为它有一个特别严重的问题:无法处理循环引用

像上图这样的情况,引用计数永远不为 0,这些对象就永远不会被回收,这会严重影响回收的效果。

但是它也并不是一无是处,它的回收实时性效果更好,可以配合「可达性分析」一起使用,发挥各自的优点,在不同的场景下使用不同的策略。

由于,「可达性分析」思路是主流,所以后续发展出来的很多回收算法都以这个思路为基础的,三色标记法就是其中之一。我们今天主要来聊聊它。

/02  三色标记法/

在讲具体原理之前先了解一个概念,「Stop The World 」,简称「STW」。

垃圾回收器的工作流程大体如下:

  1. 标记出哪些对象是存活的,哪些是可回收的。

  2. 进行回收(清除/复制/整理)。如果在回收期间有移动过的对象(复制/整理),还需要更新引用。

第一步做标记的过程又可以分成两个步骤。

  1. 标记 GC ROOT 能关联到的对象。这里会 STW。

  2. 从 GCRoots 的直接关联对象开始遍历整个对象图。这里不会STW。

垃圾回收算法主要做的就是第一步中的第二步,三色标记法也不例外,它将从GC Roots 开始遍历的对象标记为以下三种颜色:

  • 白色,初始值。本次回收没被扫描过的对象默认都是白色的。而确认不可达的对象也是白色,但是会被标记「不可达」。

  • 灰色,中间状态。本对象有被外部引用,但是本对象引用的其它对象尚未全部检测完。

  • 黑色,本对象有被其它对象引用,且已检测完本对象引用的其它对象。

其实这三种颜色是啥不重要的,重要的是它们所表达的状态,灰色的中间状态,标记过程结束后只会存在白色或者黑色。

整个过程中,这些状态是如下图这样变化的。

看似很完美的解决方案,其实也存在的一个问题:标记过程中,对象引用发生了变化。

它会导致两个问题,「多标」和「漏标」。

多标就是下图这样:

由于步骤2不会STW,所以可能存在扫描过A将它标记为黑色后,又重新引用了一个原本已经被标记为白色的D(C断开了与D的引用)。此时,D就会被回收掉,导致程序出现意料之外的bug。

「漏标」就是这样:

对象 E/F/G 是“应该”被回收的。然而因为 E 已经变为灰色了,其仍会被当作存活对象继续遍历下去。最终的结果是:这部分对象仍会被标记为存活,即本轮 GC 不会回收这部分内存。

传统的解决这两个问题的思路有两个:

  1. 在断开引用的时候做额外处理。

  2. 在「黑色」对象重新建立「白色」对象的引用时做额外处理。(回收开始后新建的对象默认为黑色)。

第一个思路专业叫法是「写屏障」,第二个是「读屏障」。其实名字就是噱头,你可以把它们俩当我们平时编程中用到的 AOP 概念来理解,在修改和读取之前做一些操作。

基于「写屏障」,可以延伸出两个方案:

  • 增量更新(Incremental Update)。针对新增的引用,将其记录下来等待重新遍历。这个操作在「修改操作后」进行,JVM 中的 CMS 垃圾回收器就是这个思路。

  • 原始快照(Snapshot At The Beginning,SATB)。当某个时刻 的 GC Roots 确定后,当时的对象图就已经确定了。如果期间发生变化,则可以记录起来,保证标记依然按照原本的视图来。这个操作在「修改操作前」进行,JVM中 的 G1 垃圾回收器用的就是这个思路。理论上,配合 「Remembered Set」,SATB 的效率是比增量更新要高的,不过会消耗更多的内存。

基于「读屏障」的方案是:在「黑色」对象重新建立「白色」对象的引用前,将这个白色对象记录下来,避免被回收掉。这个动作在「读取操作前」进行,JVM 中的 ZGC 垃圾回收器就是这个思路。

在 Golang(1.8版本之后)里,用的是一种新的机制,称之为「混合写屏障」机制。它的思路总结下来就是4句话:

  1. 将对象分为堆上的对象和栈上的对象。

  2. GC 开始将栈上的对象全部扫描并标记为黑色,无需 STW。并且之后不再进行第二次重复扫描

  3. 在 GC 期间,任何在栈上创建的新对象,均为黑色。

  4. 在 GC 期间,在堆上被删除或者添加的对象都标记为灰色。后续继续扫描。

你看,其实这些原理也没那么复杂,我相信只要你搞清楚了自己面对的是什么问题,你也能想到这些方案。

好了,总结一下。

这篇呢,Z 哥和你分享了我对 Golang 中的 GC 机制「三色标记法」的了解。

GC 的底层判断对象存活思路主要是两个,引用计数和可达性分析。由于引用计数存在循环引用问题,所以大多数 GC 都是按照后者的思路实现的,Golang 也不例外。

「三色标记法」的原理是,将对象分为了三种状态:

  • 白色,默认值。本次回收没被扫描过的对象都是白色的。确认不可达的对象也是白色,但是会被标记「不可达」。

  • 灰色,中间状态。本对象有被外部引用,但是本对象引用的其它对象尚未全部检测完。

  • 黑色,本对象有被其它对象引用,且已检测完本对象引用的其它对象。

最终将白色状态的对象回收掉。为了解决其中会存在的漏标、多标问题,它通过「混合写屏障」的机制来解决。思路是,

  1. 将对象分为堆上的对象和栈上的对象。

  2. GC 开始将栈上的对象全部扫描并标记为黑色,无需 STW。并且之后不再进行第二次重复扫描

  3. 在 GC 期间,任何在栈上创建的新对象,均为黑色。

  4. 在 GC 期间,在堆上被删除或者添加的对象都标记为灰色。后续继续扫描。

希望对你有所帮助。

推荐阅读:

  • 如何做好知识管理

  • 一些微服务拆分的浅见

原创不易,如果你觉得这篇文章还不错,就「点赞」或者「在看」一下吧,鼓励我的创作 :)

也可以分享我的公众号名片给有需要的朋友们。

如果你有关于软件架构、分布式系统、产品、运营的困惑

可以试试点击「阅读原文

这篇关于聊聊Go的三色标记法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/353000

相关文章

Golang基于内存的键值存储缓存库go-cache

《Golang基于内存的键值存储缓存库go-cache》go-cache是一个内存中的key:valuestore/cache库,适用于单机应用程序,本文主要介绍了Golang基于内存的键值存储缓存库... 目录文档安装方法示例1示例2使用注意点优点缺点go-cache 和 Redis 缓存对比1)功能特性

Go 1.23中Timer无buffer的实现方式详解

《Go1.23中Timer无buffer的实现方式详解》在Go1.23中,Timer的实现通常是通过time包提供的time.Timer类型来实现的,本文主要介绍了Go1.23中Timer无buff... 目录Timer 的基本实现无缓冲区的实现自定义无缓冲 Timer 实现更复杂的 Timer 实现总结在

Go使用pprof进行CPU,内存和阻塞情况分析

《Go使用pprof进行CPU,内存和阻塞情况分析》Go语言提供了强大的pprof工具,用于分析CPU、内存、Goroutine阻塞等性能问题,帮助开发者优化程序,提高运行效率,下面我们就来深入了解下... 目录1. pprof 介绍2. 快速上手:启用 pprof3. CPU Profiling:分析 C

使用Go语言开发一个命令行文件管理工具

《使用Go语言开发一个命令行文件管理工具》这篇文章主要为大家详细介绍了如何使用Go语言开发一款命令行文件管理工具,支持批量重命名,删除,创建,移动文件,需要的小伙伴可以了解下... 目录一、工具功能一览二、核心代码解析1. 主程序结构2. 批量重命名3. 批量删除4. 创建文件/目录5. 批量移动三、如何安

Go路由注册方法详解

《Go路由注册方法详解》Go语言中,http.NewServeMux()和http.HandleFunc()是两种不同的路由注册方式,前者创建独立的ServeMux实例,适合模块化和分层路由,灵活性高... 目录Go路由注册方法1. 路由注册的方式2. 路由器的独立性3. 灵活性4. 启动服务器的方式5.

Go语言中三种容器类型的数据结构详解

《Go语言中三种容器类型的数据结构详解》在Go语言中,有三种主要的容器类型用于存储和操作集合数据:本文主要介绍三者的使用与区别,感兴趣的小伙伴可以跟随小编一起学习一下... 目录基本概念1. 数组(Array)2. 切片(Slice)3. 映射(Map)对比总结注意事项基本概念在 Go 语言中,有三种主要

Go Mongox轻松实现MongoDB的时间字段自动填充

《GoMongox轻松实现MongoDB的时间字段自动填充》这篇文章主要为大家详细介绍了Go语言如何使用mongox库,在插入和更新数据时自动填充时间字段,从而提升开发效率并减少重复代码,需要的可以... 目录前言时间字段填充规则Mongox 的安装使用 Mongox 进行插入操作使用 Mongox 进行更

Go语言利用泛型封装常见的Map操作

《Go语言利用泛型封装常见的Map操作》Go语言在1.18版本中引入了泛型,这是Go语言发展的一个重要里程碑,它极大地增强了语言的表达能力和灵活性,本文将通过泛型实现封装常见的Map操作,感... 目录什么是泛型泛型解决了什么问题Go泛型基于泛型的常见Map操作代码合集总结什么是泛型泛型是一种编程范式,允

基于Go语言实现一个压测工具

《基于Go语言实现一个压测工具》这篇文章主要为大家详细介绍了基于Go语言实现一个简单的压测工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录整体架构通用数据处理模块Http请求响应数据处理Curl参数解析处理客户端模块Http客户端处理Grpc客户端处理Websocket客户端

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操