Skip to Content
Go 语言GC根对象

根对象

bitmap 在 GC 中的作用是什么?

🔍 我们先厘清几个关键概念:

一、什么是 root?

GC 的「扫描起点」,包括:

  • 全局变量(global data
  • 当前运行的 goroutine 栈上的局部变量(stack roots)
  • 寄存器中可能的临时值(编译器插入)
  • 堆上通过特殊机制保持引用的对象(比如 channel、interface 等)

这些 root 是 GC 标记阶段的 入口点


二、那 bitmap 在哪里发挥作用?

GC 从 root 开始,并不是直接扫描整个堆,而是:

  1. 取出一个 root 指向的地址,比如 p := new(Person)
  2. 去堆上找 p 指向的对象
  3. 然后 → 看这个对象有没有指针字段?哪些字段是指针?

这时候就要靠 bitmap(或者 _type 信息 + bitmap)来判断这个对象:

  • 是不是对象的起始地址?
  • 是不是包含指针?
  • 哪些字段是指针?需要递归追踪

三、所以你可以这样理解

GC 会扫描所有的 root,但不是“扫描整个内存”,而是

  • 只扫描 root 指向的对象
  • 对象里哪些字段是指针,由 bitmap(和类型信息)精确告诉 GC
  • 再根据这些指针继续向下扫描(形成对象图)

四、举个更形象的例子

type Node struct { value int next *Node } head := new(Node) // 构建链表:head → n1 → n2 → nil

GC 扫描过程:

  1. root 中有一个变量 head,GC 从 head 开始
  2. head 是个对象 → bitmap 显示它是对象起点,且包含一个指针字段 next
  3. 读出 next,发现它指向 n1
  4. 重复第 2 步,直到 next == nil

整个扫描过程中:

  • 只扫描了实际链表使用的对象
  • 没有扫描堆上未被引用的内存块(避免了“扫整个堆”的低效)

五、总结你的问题

你问的点解答
GC 是不是扫描所有内存?❌ 不是。只从 root 出发,递归扫描能访问到的对象
bitmap 在哪起作用?用于判断一个地址是不是对象起点,以及对象中哪些字段是指针
root 是怎么影响 GC 范围的?root 决定了 GC 的“入口点”,bitmap 决定了“扫描范围”

root 具体是在哪里?GC 怎么知道去哪里找这些 root?

这个问题非常棒,涉及了 Go GC 的核心机制之一:「Root Set 的识别与遍历」。我们现在就来一层一层讲清楚:


一、Root 到底是什么?在哪?

GC 中的 root(根对象) 是指一切 程序运行中直接可达的引用。Go 中具体包括:

类型举例存在的位置
栈上的变量函数局部变量、参数goroutine 的 stack 内存中
全局变量包级变量.data.bss 段(全局静态内存)
寄存器值编译器保存在栈上的寄存器备份栈上特定区域
特殊 runtime 结构channel、select、reflect、defer、panic堆内的一些特殊结构

二、GC 是怎么知道去哪里找这些 root 的?

总体过程是:Go 编译器 + runtime 协作实现精确扫描


1. 栈上的变量(stack roots)

每个 goroutine(G)都有自己的栈内存。
Go 编译器在编译函数时会生成一个 栈的指针图(stack map),告诉 GC:

  • 这个函数调用时,栈上哪些位置是指针
  • 哪些位置是普通值,GC 不需要管

GC 扫描 goroutine 的时候,就按照 stack map 去栈上找指针。

🔧 实现细节:

  • Go 在 runtime.g 结构中维护了所有 G 的信息
  • 扫描从 allgs(所有 goroutine)遍历每个 G 的栈
  • 对于每个 G,用 stack map 扫其栈上的活跃帧

源码片段:

// runtime/mgcmark.go func scanstack(gp *g) { ... } // 扫描 goroutine 栈

2. 全局变量(global data)

全局变量存放在两个段中:

  • .data:有初始值的变量
  • .bss:未初始化的全局变量

Go 编译器会生成一个 bitmap(和 stack map 类似),用于描述:

  • .data / .bss 段中,哪些位置是指针

GC 只需要从 bitmap 中知道哪些变量需要追踪指针字段。

源码位置:

runtime/mgcmark.goscanGlobals()
// 使用 `dataBitmap` 遍历全局变量段 markRoots.addSpan("data", datastart, dataBitmap)

3. 寄存器中的指针(GC safepoint)

这个最特殊。因为运行中的程序有些变量存在 CPU 寄存器里(而不是内存中),怎么办?

Go 编译器在特定的 GC safepoint(安全点) 会把寄存器里的值保存到栈上。

例如:

MOVQ AX, -8(SP)

然后 GC 就可以通过 stack map 找到这些保存位置。

这就是 精确 GC(precise GC) 的基础。


4. runtime 特殊结构中的 root

比如:

  • channel 中存的值(runtime.hchan
  • select 结构中的引用
  • interface 指向的对象
  • finalizer 列表等

这些结构 GC 不能直接扫,它们的字段结构和引用方式不定。

→ Go 使用自定义的扫描函数(称为 “GC assist”“specials”)来处理它们。

例如:

// 扫描 channel 中的值 scanblock(h.recvq, ...)

这些结构在堆上,runtime 会维护一个列表(如 specials list),每次 GC 会调用特定函数去遍历它们的引用。


三、总结一下(一图流)

┌────────────────────────────┐ │ GC root set │ └────────────────────────────┘ ┌───────────┼──────────────────────┐ │ │ │ .data/.bss goroutine stack runtime 特殊结构 (bitmap标记) (stack map 标记) (手动注册/assist函数)

GC 开始时会:

  1. 遍历 allgs,读取每个 goroutine 的栈指针
  2. 扫描 .data.bss 的指针变量
  3. 进入 runtime 的特殊结构辅助扫描
  4. 从这些 root 向下递归扫描对象图(通过 bitmap 和类型信息)

与 ChatGPT 的对话

Last updated on