news 2026/4/18 12:38:59

垃圾回收 (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/4/18 6:29:44

【预测模型】基于GA-HIDMSPSO算法优化BP神经网络+NSGAII多目标优化算法工艺参数优化、工程设计优化(四目标优化案例)附Matlab代码

✅作者简介:热爱科研的Matlab仿真开发者,擅长数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。 🍎 往期回顾关注个人主页:Matlab科研工作室 👇 关注我领取海量matlab电子书和数学建模资料 &#…

作者头像 李华
网站建设 2026/4/18 6:30:47

PCB成型毛刺:从根源控制告别烦恼

PCB 成型后边缘的毛刺,是工程师们最头疼的问题之一。毛刺不仅影响板子的美观,还可能导致短路、划伤元器件,甚至影响产品的可靠性。很多人遇到毛刺,第一反应是 “打磨处理”,但打磨不仅增加了工序成本,还可能…

作者头像 李华
网站建设 2026/4/18 6:28:41

整合单细胞与空间转录组学解析非小细胞肺癌免疫微环境异质性

一、摘要与引言 肺癌是全球范围内发病率位居第二且癌症相关死亡率最高的恶性肿瘤,其复杂的肿瘤生态系统涵盖多种免疫细胞类型。研究表明,骨髓来源细胞,尤其是巨噬细胞,在疾病进展过程中扮演关键角色。为进一步探究肺腺癌&#xf…

作者头像 李华
网站建设 2026/4/18 6:29:42

系统更新后残留的文件在C盘哪个地方?如何清理呢?

theme: default themeName: 默认主题在进行一次大的系统更新后,你可能会发现电脑变慢了,或者c盘空间莫名变少,这通常是因为更新过程留下了一些临时文件,旧的系统备份,以及没有自动移除的无用组件,这些残留文件会弄乱你的c盘,占用本可以用来存放程序,文档和照片的宝贵空间,找到并…

作者头像 李华
网站建设 2026/4/18 11:00:35

SpringData JPA 都能写 SQL,为啥还要用 MyBatis?

SpringData JPA 都能写 SQL,为啥还要用 MyBatis? 之前聊过JPA和MyBatis的核心区别,但总觉得没说透。实际开发里,很多人纠结选哪个,不是因为不知道“JPA面向对象、MyBatis面向SQL”,而是踩过具体的小坑后才…

作者头像 李华
网站建设 2026/4/18 8:35:36

【直播预告】 复刻高德地图导航——GIS开发实战直播来袭!

如果你希望掌握WebGIS开发的核心技能,提升自己在GIS领域的竞争力。本周四下午2点,我们将带来一场适合webgis小白学习的技术直播,使用Vue框架开发高德地图的导航功能。适合人群:对GIS开发感兴趣、想从事地图开发的学生/在职人员。无…

作者头像 李华