GO——垃圾回收机制学习

标记-清除

go1.5之前使用的策略

  1. 核心流程
    标记阶段(Mark):
    从根对象(全局变量、栈、寄存器中的指针等)出发,递归遍历所有可达对象并标记为存活。

    清除阶段(Sweep):
    遍历堆内存,回收未被标记的对象(即不可达的垃圾对象)。

  2. STW(Stop-The-World)问题
    全程暂停:在标记和清除阶段,用户程序需要完全停止(STW),导致显著的延迟。
    例如,在 Go 1.0 中,即使堆内存很小,STW 时间也可能达到几十毫秒,这对高并发服务影响较大。

性能瓶颈

  • 长暂停时间:早期版本的 STW 时间随堆内存增长而线性增加,无法满足低延迟需求。

  • 内存碎片:清除阶段后可能产生内存碎片,影响内存分配效率。

三色标记-清除

go1.5之后使用的策略。支持并发执行,极大降低gc所需时间。

颜色标记定义

  • 白色:未被访问的对象(待回收)。

  • 灰色:已访问但子对象未检查。

  • 黑色:已访问且子对象已检查。

执行过程

1. 初始标记(Mark Setup)

触发条件:堆内存达到阈值(由 GOGC 控制)或定时触发。

操作步骤:
* 暂停程序(STW):冻结所有 Goroutine。

* 标记根对象:扫描栈、全局变量、寄存器中的指针,标记为 灰色。

* 启动写屏障:启用混合写屏障,为并发标记阶段记录引用变更。

* 恢复程序运行:STW 结束(通常 <1ms)。	
// 伪代码示例
func GCStart() {
    stopTheWorld()
    markRoots()    // 标记根对象(灰色)
    enableWriteBarrier()
    startTheWorld()
    startConcurrentMarking() // 启动并发标记协程
}

2. 并发标记(Concurrent Marking)

目标:
递归标记所有可达对象,与用户程序并发执行。

关键机制:
混合写屏障(Hybrid Write Barrier)。

流程:

  • 从灰色对象出发:

  • 取出一个灰色对象,扫描其引用的所有子对象。

  • 将子对象标记为 灰色(若未被标记)。

  • 当前对象标记为 黑色。

写屏障处理:

用户代码修改对象引用时(如 A → B 变为 A → C),写屏障会捕获变更:

  • 若旧值 B 是白色,标记为灰色。

  • 若新值 C 是白色,标记为灰色。

  • 重复直到无灰色对象:

  • 当所有灰色对象处理完毕时,标记完成。

// 并发标记协程(简化逻辑)
func concurrentMark() {
    for {
        obj := getGreyObject()
        if obj == nil {
            break // 无灰色对象,标记完成
        }
        for _, child := range obj.Children() {
            if child.color == white {
                child.color = grey
            }
        }
        obj.color = black
    }
}

3. 标记终止(Mark Termination)

触发条件:
所有灰色对象处理完毕。

操作步骤:

  • 暂停程序(STW):短暂暂停(通常 <1ms)。

  • 重新扫描栈:检查在并发标记期间栈上的新引用变更。

  • 确认标记完成:确保所有存活对象标记为黑色,白色对象为待回收。

  • 关闭写屏障:禁用写屏障,准备清除阶段。

  • 恢复程序运行。

4. 并发清除(Concurrent Sweeping)

目标:
回收所有白色对象(不可达对象)的内存。

流程:

  • 遍历堆内存:逐个内存块检查对象颜色。

  • 回收白色对象:将白色对象的内存加入空闲列表,供后续分配复用。

  • 重置颜色标记:将所有黑色对象重置为白色,为下次 GC 准备。

// 清除阶段伪代码
func sweep() {
    for each memoryBlock in heap {
        for each object in block {
            if object.color == white {
                free(object)
            } else {
                object.color = white // 重置颜色
            }
        }
    }
}

执行流程与标记-清除的区别

// Go 1.4 及之前的 GC 行为(伪代码)
func GC() {
    StopTheWorld()
    mark()  // 标记所有可达对象
    sweep() // 清除不可达对象
    StartTheWorld()
}

// Go 1.5 及之后的 GC 行为(伪代码)
func GC() {
    StopTheWorld()          // 初始标记(短暂 STW)
    startConcurrentMark()   // 并发标记(与用户程序并行)
    StopTheWorld()          // 标记终止(短暂 STW)
    sweep()                 // 并发清除
    StartTheWorld()
}

跟标记-清除对比

版本 GC 机制 STW 时间 核心改进
1.0-1.4 标记-清除法 几十毫秒级别 并行标记优化
1.5+ 并发三色标记-清除法 亚毫秒级别 并发标记、混合写屏障

并发执行

为何需要写屏障

在 并发标记阶段(GC 标记与用户程序同时运行),用户代码可能修改对象间的引用关系,导致以下问题:

  • 漏标:已标记的对象被修改引用,导致其子对象未被标记(错误回收)

误标:未标记的对象被错误标记(内存泄漏)

写屏障的作用是 在对象引用被修改时,记录这些变更,确保标记阶段的正确性。

混合写屏障

规则

  • 写前操作:若被覆盖的旧值(*slot 原指向的对象)是白色(未标记),则将其标记为灰色。

  • 写后操作:若新值(ptr)是白色,则将其标记为灰色。

func writePointer(slot *unsafe.Pointer, ptr *Object) {
    // 写前:标记旧值为灰色(若为白色)
    old := *slot
    if isWhite(old) {
        shade(old)
    }
    
    // 实际写操作
    *slot = ptr
    
    // 写后:标记新值为灰色(若为白色)
    if isWhite(ptr) {
        shade(ptr)
    }
}

你可能感兴趣的:(golang,学习)