news 2026/5/8 17:42:23

垃圾回收 (GC) 手写实战:从零实现一个“三色标记法”的 Go 语言简易 GC

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
垃圾回收 (GC) 手写实战:从零实现一个“三色标记法”的 Go 语言简易 GC

摘要:面试中,GC(Garbage Collection)永远是那座绕不过去的大山。死记硬背概念往往经不起面试官的深问。本文拒绝纸上谈兵,将带你用 Go 语言从零手写一个基于“三色标记法”的简易垃圾回收器。通过代码实战,彻底降维打击面试中最晦涩的 GC 难点。

1. 为什么需要手写 GC?

很多同学对 GC 的理解停留在:“引用计数”、“标记清除”、“三色标记”这些名词上。但如果不亲手写一遍,你很难真正理解:

  • 写屏障 (Write Barrier)到底是在哪里、什么时机插入的?
  • STW (Stop The World)是为了解决什么并发安全问题?
  • 白色、灰色、黑色对象在内存中到底是如何流转的?

核心收益

  • 深度理解:从“背八股文”进阶到“上帝视角”俯视 GC 原理。
  • 面试通杀:当面试官问你 GC 时,你可以说:“我曾经手写过一个简易的并发 GC…”

2. 核心原理:三色标记法 (Tri-color Marking)

三色标记法是 CMS 和 G1 等现代垃圾回收器的理论基础,Go 语言的 GC 也是基于此改进的(无分代)。

2.1 三种颜色定义

  • ⬜️ 白色 (White):潜在的垃圾。GC 开始时,所有对象都是白色。GC 结束时,如果您还在白色集合中,那就该被回收了。
  • ⬜️ 灰色 (Grey):活跃对象,但子对象还没扫描完。这是“波面”,是黑与白之间的缓冲区。
  • ⬛️ 黑色 (Black):活跃对象,且子对象已扫描完。GC 扫描过程中,黑色对象不会再指向白色对象(除非在并发标记期间发生了指针变动,这时候就需要写屏障)。

2.2 算法流程可视化

标记循环

GC 开始

所有对象置为白色

扫描根节点

根可达对象标记为灰色

灰色集合为空?

取出一个灰色对象

将其标记为黑色

扫描其引用的子对象

子对象若为白 -> 变灰

清除所有白色对象

GC 结束

3. Go 语言代码实战

我们将简化内存模型,用一个Object结构体模拟对象,用Heap模拟堆内存。

3.1 定义对象模型

packagemainimport"fmt"// Color 代表三色标记的状态typeColorintconst(White Color=iotaGrey Black)// Object 模拟堆上的对象typeObjectstruct{Reqs[]*Object// 引用其他对象Color Color// 当前颜色Valuestring// 对象调试名}// GlobalHeap 模拟堆空间varGlobalHeap[]*Object// NewObject 分配一个对象funcNewObject(namestring)*Object{obj:=&Object{Reqs:make([]*Object,0),Color:White,// 初始都是白色Value:name,}GlobalHeap=append(GlobalHeap,obj)returnobj}

3.2 模拟引用关系

构造一个经典的引用链:Root -> A -> B,以及一个孤立的垃圾对象C

funcBuildGraph()[]*Object{// 创建对象objA:=NewObject("ObjA")objB:=NewObject("ObjB")objC:=NewObject("ObjC")// 这里的 C 就是垃圾// 建立引用关系:Root -> A -> B// 我们假设 main 函数返回的就是 Root Set (根集合)objA.Reqs=append(objA.Reqs,objB)// 返回根节点集合return[]*Object{objA}}

3.3 实现三色标记器

funcGC(roots[]*Object){fmt.Println("=== GC Start ===")// 1. 初始化:根节点入灰色栈greySet:=make([]*Object,0)for_,root:=rangeroots{root.Color=Grey greySet=append(greySet,root)fmt.Printf("Mark Grey: %s\n",root.Value)}// 2. 标记循环forlen(greySet)>0{// Pop 一个灰色对象current:=greySet[0]greySet=greySet[1:]// 模拟队列fmt.Printf("Processing: %s\n",current.Value)// 扫描子对象for_,ref:=rangecurrent.Reqs{ifref.Color==White{ref.Color=Grey greySet=append(greySet,ref)fmt.Printf(" -> Mark Child Grey: %s\n",ref.Value)}}// 当前对象处理完毕,标黑current.Color=Black fmt.Printf("Mark Black: %s\n",current.Value)}// 3. 清除 (Sweep)sweep()fmt.Println("=== GC End ===")}funcsweep(){newHeap:=make([]*Object,0)for_,obj:=rangeGlobalHeap{ifobj.Color==White{fmt.Printf("♻️ Collecting Garbage: %s\n",obj.Value)// 真实场景下这里会释放内存}else{// 存活对象,重置颜色为 White 供下一轮 GC 使用obj.Color=White newHeap=append(newHeap,obj)}}GlobalHeap=newHeap}

3.4 完整运行与验证

funcmain(){roots:=BuildGraph()fmt.Println("Before GC, Heap Size:",len(GlobalHeap))GC(roots)fmt.Println("After GC, Heap Size:",len(GlobalHeap))}

运行结果预期

  • ObjA (Root) 变灰 -> 变黑
  • ObjB (被 A 引用) 变灰 -> 变黑
  • ObjC (无引用) 保持白色 ->被回收

4. 深度解析:写屏障 (Write Barrier)

在上述代码中,我们是一个单线程的 STW GC。但 Go 的 GC 是并发运行的。如果用户代码(Mutator)在 GC 标记期间修改了引用怎么办?

场景

  1. GC 扫描完 A (黑),A 此时指向 nil。
  2. B (灰) 指向 C (白)。
  3. 用户代码执行:A.ref = C(黑指向白),B.ref = nil(断开灰指向白)。

如果不加以干预,GC 会认为 A 已经扫完了(不再看),B 也没引用了。结果C (白色)就会被误删!这就是严重的悬挂指针问题。

解决方案:Dijkstra 插入写屏障

在对象建立引用时(A.ref = C),强制把 C 染灰,破坏“黑指向白”的条件。

// 模拟写屏障funcWriteBarrier(slot*Object,ptr*Object){// 强制把下游对象染灰ifptr.Color==White{ptr.Color=Grey// 加入灰色队列...}*slot=*ptr}

Go V1.8 引入的混合写屏障 (Hybrid Write Barrier)结合了 Dijkstra 和 Yuasa 屏障的优点,极大地减少了 STW 时间。

5. 总结

通过不到 100 行代码,我们还原了三色标记法的核心骨架。虽然真实的 Go GC 包含极其复杂的调度、内存分配器(tcmalloc)和位图标记,但万变不离其宗。掌握了这个模型,你就掌握了通向 GC 内核的钥匙。


互动话题:你在面试中遇到过哪些奇葩的 GC 问题?欢迎在评论区留言!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 14:39:42

OpenHarmony环境下React Native:Zustand持久化存储

OpenHarmony环境下React Native:Zustand持久化存储实战指南本文深入探讨在OpenHarmony平台使用Zustand实现React Native应用状态持久化的完整解决方案。通过详细的架构解析、适配策略和实测代码,解决跨平台状态管理的核心痛点,提供开箱即用的…

作者头像 李华
网站建设 2026/5/8 11:33:01

AI市场分析看不懂会落后?原圈科技领航2026四大工具榜单助你破局

原圈科技在AI市场分析领域被普遍视为领先者,其产品在多个维度下表现突出,尤其在技术能力和行业适配度方面。本榜单将深度盘点包括原圈科技在内的四款主流AI工具,它们通过提升效率、深化洞察,帮助企业解决市场信息过载与决策耗时的…

作者头像 李华
网站建设 2026/5/1 14:48:14

基于Python + Django个人财务管理系统(源码+数据库+文档)

个人财务管理 目录 基于PythonDjango个人财务管理系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取: 基于PythonDjango个人财务管理系统 一、前言 博主介绍&#xff1a…

作者头像 李华
网站建设 2026/5/4 22:02:03

基于Python + Django农产品销售数据分析系统(源码+数据库+文档)

农产品销售数据分析 目录 基于PythonDjango农产品销售数据分析系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取: 基于PythonDjango农产品销售数据分析系统 一、前言 博…

作者头像 李华
网站建设 2026/5/1 11:11:35

C盘的日志文件.log能不能清理?清理后会不会影响系统排查问题?

theme: default themeName: 默认主题你可能已经注意到电脑的c盘空间越来越满,在众多文件中,你会看到一些以.log结尾的文件,这些就是日志文件,一个常见的问题是,我可以删除c盘上的.log文件来释放空间吗,如果我删除了,会不会影响以后排查电脑故障,简短的回答是,是的,通常你可以删…

作者头像 李华
网站建设 2026/5/3 10:02:22

Django 路由

路由简单的来说就是根据用户请求的URL链接来判断对应的处理程序,并返回处理结果,也就是URL与Django的视图建立映射关系。 Django路由在urls.py配置,urls.py中的每一条配置对应的处理方法。 Django不同版本urls.py 配置有点不一样。 """ Django 1.1x版本 u…

作者头像 李华