根对象
bitmap 在 GC 中的作用是什么?
🔍 我们先厘清几个关键概念:
一、什么是 root?
GC 的「扫描起点」,包括:
- 全局变量(
global data) - 当前运行的 goroutine 栈上的局部变量(stack roots)
- 寄存器中可能的临时值(编译器插入)
- 堆上通过特殊机制保持引用的对象(比如 channel、interface 等)
这些 root 是 GC 标记阶段的 入口点。
二、那 bitmap 在哪里发挥作用?
GC 从 root 开始,并不是直接扫描整个堆,而是:
- 取出一个 root 指向的地址,比如
p := new(Person) - 去堆上找
p指向的对象 - 然后 → 看这个对象有没有指针字段?哪些字段是指针?
这时候就要靠 bitmap(或者 _type 信息 + bitmap)来判断这个对象:
- 是不是对象的起始地址?
- 是不是包含指针?
- 哪些字段是指针?需要递归追踪
三、所以你可以这样理解
GC 会扫描所有的 root,但不是“扫描整个内存”,而是:
- 只扫描 root 指向的对象
- 对象里哪些字段是指针,由 bitmap(和类型信息)精确告诉 GC
- 再根据这些指针继续向下扫描(形成对象图)
四、举个更形象的例子
type Node struct {
value int
next *Node
}
head := new(Node)
// 构建链表:head → n1 → n2 → nilGC 扫描过程:
- root 中有一个变量
head,GC 从head开始 head是个对象 → bitmap 显示它是对象起点,且包含一个指针字段next- 读出
next,发现它指向n1 - 重复第 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.go → scanGlobals()// 使用 `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 开始时会:
- 遍历
allgs,读取每个 goroutine 的栈指针 - 扫描
.data和.bss的指针变量 - 进入 runtime 的特殊结构辅助扫描
- 从这些 root 向下递归扫描对象图(通过 bitmap 和类型信息)