目录

Go 原理之 gc 垃圾回收机制:三色标记 + 混合写屏障(需要 STW)

用过 Go 开发的都知道,它的垃圾回收(GC)机制是保障程序稳定运行的关键——尤其是在高并发场景下,GC 的延迟直接影响服务的响应性能。

Go 1.5 版本后,GC 机制逐步演进为“三色标记 + 混合写屏障”的组合方案,将 STW(Stop The World,世界暂停)时间压缩到毫秒级甚至微秒级。

今天,我们从原理到源码,彻底讲透这套 GC 方案的设计逻辑与实现细节。

一、常见垃圾回收算法

在学习 Go Gc 垃圾回收算法之前,我们先对现有的比较常见的垃圾回收算法做个大概了解,如下表:

垃圾回收算法 描述 代表语言 优缺点
引用计数 为每个对象维护一个引用计数,记录对象被引用的次数
每当一个对象被引用时,引用计数就会增加。
当对象不再被引用时,引用计数就会减少。
如果对象的引用计数变为 0,
则对象可以被垃圾回收器回收
PythonPHP 优点
实现简单,处理快
缺点
无法处理循环引用,两个对象相互引用,计数永远不为0
分代收集 按照对象生命周期长短划分不同的代空间,
生命周期长的放入老年代,短的放入新生代,
不同代有不同的回收算法和回收频率
Java 优点
性能好
缺点
需要 STW,算法复杂
三色标记法 从根变量开始遍历所有引用的对象,标记引用的对象为不同颜色,
被标记为白色的对象进行回收
Golang 优点
解决了引用计数的缺点
缺点
需要 STW,暂时停掉程序运行

⚠️:以上都需要 STW

二、Go GC 为什么要“三色标记 + 混合写屏障”?

在聊具体方案前,得先搞清楚一个核心问题:Go 为什么要花大力气迭代 GC 机制?这要从早期 GC 方案的痛点说起。

Go 1.0 采用“标记-清除”算法,全程 STW——执行 GC 时,所有 Goroutine 都会暂停,直到 GC 完成。

这种方式在程序内存较小时还好,一旦内存达到 GB 级,STW 时间会长达数百毫秒,严重影响服务可用性。

为了解决 STW 过长的问题,Go 团队引入了“并发标记”的思路:让 GC 标记过程与 Goroutine 并发执行,只在关键阶段短暂 STW。

但并发标记会带来新问题——对象引用关系动态变化(比如 Goroutine 在标记过程中修改了对象的引用),可能导致“漏标记”或“误标记”。

为解决并发标记的一致性问题,业界有两种经典方案:

  1. 写屏障:监控对象引用的修改,当修改发生时标记相关对象,保证标记一致性;
  2. 快照:在标记开始时生成对象引用的快照,基于快照进行标记,但会占用额外内存。

Go 选择了写屏障方案,并结合“三色标记”算法提升标记效率,最终在 Go 1.8 版本确定了“三色标记 + 混合写屏障”的成熟方案——既实现了大部分阶段的并发执行,又通过混合写屏障解决了传统写屏障的不足,将 STW 时间控制在极低水平。

三、三色标记与混合写屏障是什么?

要理解 Go GC 的核心逻辑,必须先吃透“三色标记”和“混合写屏障”这两个核心概念。

它们一个负责“高效标记垃圾”,一个负责“保证并发标记的一致性”,相辅相成。

(一)三色标记

三色标记:高效区分“存活对象”与“垃圾”

三色标记算法是一种基于“对象可达性”的标记方法,通过给对象标记三种颜色,区分不同状态的对象,实现高效的并发标记。

三种颜色对应对象的三种状态,核心逻辑围绕“可达性分析”展开——从根对象(如 Goroutine 栈上的变量、全局变量)出发,遍历所有可达对象,未被遍历到的就是垃圾。

三种颜色的定义及流转逻辑:

颜色 状态定义 处理逻辑
白色 未被标记的对象 初始状态,所有对象都是白色;标记结束后仍为白色的是垃圾,将被清理
灰色 已被标记,但引用的子对象未完全标记 处于“待处理”状态,会被放入标记队列,后续需遍历其引用的子对象
黑色 已被标记,且引用的子对象已全部标记 处于“已完成”状态,不会再被遍历;黑色对象是存活对象,标记阶段不会被清理

核心流转流程:

  1. 初始时,所有对象为白色,根对象(如 Goroutine 栈变量、全局变量)被标记为灰色,放入标记队列;
  2. 从标记队列取出灰色对象,遍历其引用的子对象:将白色子对象标记为灰色并加入队列,然后将当前灰色对象标记为黑色;
  3. 重复步骤 2,直到标记队列为空;
  4. 标记结束,白色对象即为垃圾。

源码中对象标记的核心结构体(简化版,来自 src/runtime/mheap.go):

// mspan 是 Go 内存管理的基本单元,包含多个相同大小的对象
type mspan struct {
    next *mspan         // 链表下一个节点
    prev *mspan         // 链表前一个节点
    startAddr uintptr   // 该 span 起始地址
    npages    uintptr   // 占用的页数
    sizeclass uint8     // 对象大小等级(对应固定的对象大小)
    allocBits  *gcBits  // 分配位图:标记哪些对象已被分配
    gcmarkBits *gcBits  // 标记位图:标记对象的颜色状态(三色标记的核心)
}

// gcBits 用于存储对象的标记状态(简化版)
type gcBits struct {
    bits []uint8        // 位图数组,每一位代表一个对象的标记状态
    nbits int           // 总位数(对应对象数量)
}

关键逻辑:mspan 是 Go 内存分配的基本单元,每个 mspan 管理一批相同大小的对象。gcmarkBits 位图用于记录每个对象的标记状态——比如某一位为 0 代表白色,1 代表灰色或黑色,通过位图操作实现高效的批量标记。

大白话讲解:

v1.13之前,go 使用的是 标记-清除法,需要 stw ,效率极低;

v1.15之后,go 采用 三色标记 + 混合写屏障 极大的降低stw的时间,提高gc性能

三色标记白色(清除对象) + 灰色(过渡对象,受保护, 最终变黑色) + 黑色(受保护)

可达对象引用关系举例

可达的意思就是可以关联到的,有对象引用它了

对象1 = 对象2 // 对象2可达,对象1引用了对象2,对象2 被 对象1 引用
对象1 = 对象3
对象2 = 对象3
对象2 = 对象5

三色标记-流程

    1. 初始时,所有对象被标记为白色
    1. gc 开始,遍历 rootset 根节点,将有引用对象的对象标记为 灰色,存入灰色对象列表
    1. 遍历 灰色对象,将直接可达对象标记为 灰色,并将自身标记为 黑色
    1. 重复第3步,直到标记完所有的对象 (灰色对象列表为空)
    1. 将白色对象清除,保留黑色对象

如下图:

/img/go-gc/1.png
image-20230413112723437

(二)混合写屏障

混合写屏障:解决并发标记的一致性问题。

三色标记在并发执行时,最大的问题是“对象引用被动态修改”。

比如:黑色对象 A 原本引用白色对象 B,在标记过程中,A 突然取消引用 B,而灰色对象 C 新增引用 B——此时 B 会被漏标记,最终被误判为垃圾。

为解决这个问题,需要通过“写屏障”监控对象引用的修改。

Go 之前使用过“插入写屏障”和“删除写屏障”,但各有不足:

  • 插入写屏障:当有新引用插入时(如 C 引用 B),将 B 标记为灰色,但会导致栈上对象需要最终 STW 重新扫描;
  • 删除写屏障:当有引用删除时(如 A 取消引用 B),将 B 标记为灰色,但会导致大量不必要的标记。

Go 1.8 引入的“混合写屏障”结合了两者的优势,核心规则(四句话总结):

  1. 写屏障开启时,所有新分配的对象直接标记为黑色;
  2. 当灰色对象或黑色对象修改引用,指向白色对象时,将白色对象标记为灰色;
  3. 当栈上对象修改引用时,不触发写屏障(栈上对象最终会短暂 STW 扫描);
  4. 堆上对象修改引用时,触发写屏障。

混合写屏障的优势:既避免了插入写屏障对栈的频繁扫描,又减少了删除写屏障的冗余标记,同时保证了并发标记的一致性,大幅缩短 STW 时间。

大白话讲解:

三色标记存在并发问题:

在三色标记期间,如果没有STW,并发创建对象,可能存在垃圾对象或误删对象的情况:

    1. 黑色对象的引用对象被删除,则不可达,正常黑色对象应该被回收,但是gc期间只会循环遍历灰色列表,不会回收黑色对象,因此该对象为垃圾对象 (多余垃圾对象)

      eg:对象1已经被标记为黑色,表示该对象有引用方,受保护,如果没有stw,该对象的引用可能被删除,正常应该转为白色对象被清除,然gc并不会清除黑色对象。

    1. 黑色对象引用了白色对象,白色对象有了引用对象应该被保护,但仍然被无情的回收 (清掉不该清的对象)

    ​ 白色对象只有被灰色对象引用情况,才会判断是否需要清理,白色对象如果在gc期间引用了黑色对象,那只会被误删除。

所以 go 引入了 混合写屏障 机制,满足:

  • 强三色不变式:黑色对象不允许引用了白色对象;因为一旦引用,该黑色对象将不会继续参与 gc,白色对象会被无理清除。
  • 弱三色不变式:黑色对象可以引用白色对象,但该白色对象必须被其它灰色对象或其上游有灰色对象引用,否则该白色对象将被无理清除。

这里需要注意一点,插入屏障仅会在堆内存中生效,不对栈内存空间生效,这是因为 go 在并发运行时,大部分的操作都发生在栈上,函数调用会非常频繁。

数十万 goroutine 的栈都进行屏障保护自然会有性能问题

所以 gc 期间,任何在栈上新创建的对象,均为黑色。

混合写屏障 开启期间 描述
插入写屏障 创建的新对象为灰色对象 满足:强三色不变式。
不会存在黑色对象引用白色对象
删除写屏障 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色 满足:弱三色不变式
(保护灰色对象到白色对象的路径不会断)

(三)STW

STW:不可避免的“短暂暂停”。

虽然“并发标记”是核心,但 Go GC 仍需要短暂的 STW 阶段——主要用于“标记准备”和“标记终止”,原因是这两个阶段需要操作全局状态,无法并发执行:

  1. 标记准备:暂停所有 Goroutine,初始化标记状态(如重置位图、构建根对象列表),时间通常在微秒级;
  2. 标记终止:暂停所有 Goroutine,处理剩余的灰色对象,清理标记状态,时间通常在微秒级。

混合写屏障的引入,让栈上对象无需在并发标记阶段扫描,只需在标记终止前短暂 STW 扫描一次,这是 Go GC 能将 STW 时间压缩到极低水平的关键。

(四)优缺点

优点:

    减少stw时间,三色标记需要stw整个程序,混合写屏障(分段stw)可以有效降低stw的时间

缺点:

    回收精度低,有些垃圾需要在下一轮 gc 清理

四、Go GC 完整流程

三色标记 + 混合写屏障如何协同工作?

理解了核心概念后,我们结合 Go 1.21 版本的 GC 流程,拆解“三色标记 + 混合写屏障”的完整协同逻辑。

整个流程分为四个阶段,其中只有两个阶段需要短暂 STW。

阶段 1:标记准备(STW)

此阶段会暂停所有 Goroutine(STW),执行初始化操作,耗时极短:

  1. 暂停所有 Goroutine,禁止其修改对象引用;
  2. 初始化 GC 相关全局状态,如重置 gcmarkBits 位图;
  3. 构建根对象列表(根对象包括:全局变量、Goroutine 栈上的变量、寄存器中的变量);
  4. 将根对象标记为灰色,加入标记队列;
  5. 开启混合写屏障;
  6. 恢复所有 Goroutine,结束 STW。

阶段 2:并发标记(与 Goroutine 并发)

此阶段是 GC 的核心阶段,与 Goroutine 并发执行,无需 STW:

  1. 从标记队列中取出灰色对象,遍历其引用的所有子对象;
  2. 对每个子对象,通过 gcmarkBits 检查状态:若为白色,标记为灰色并加入标记队列;
  3. 遍历完成后,将当前灰色对象标记为黑色;
  4. 同时,混合写屏障监控堆上对象的引用修改:
    • 新分配的堆对象直接标记为黑色;
    • 堆上对象修改引用时,若指向白色对象,将其标记为灰色;
  5. 当标记队列中的灰色对象为空时,并发标记阶段结束。

阶段 3:标记终止(STW)

此阶段再次短暂 STW,处理并发标记的收尾工作:

  1. 暂停所有 Goroutine;
  2. 关闭混合写屏障;
  3. 扫描所有 Goroutine 的栈(因为栈上对象修改不触发写屏障,需最终扫描确保无漏标);
  4. 处理剩余的灰色对象(遍历其引用并标记);
  5. 计算垃圾对象的数量和大小,为清理阶段做准备;
  6. 恢复所有 Goroutine,结束 STW。

阶段 4:清理阶段(并发)

此阶段与 Goroutine 并发执行,清理被标记为白色的垃圾对象:

  1. 遍历所有 mspan,通过 gcmarkBits 识别白色对象;
  2. 回收白色对象的内存,将其标记为“空闲”状态,存入对应大小等级的空闲链表;
  3. 对内存碎片进行整理(如合并相邻的空闲 mspan);
  4. 清理完成后,重置 gcmarkBits,为下一次 GC 做准备。

大白话讲解

三色标记 + 混合写屏障

  • 标记准备(Mark Setup):开启混合写屏障(Write Barrier),需 STW(stop the world)
  • 标记开始(Marking):使用三色标记法并发标记 ,与用户程序并发执行
  • 标记终止(Mark Termination):对触发写屏障的对象进行重新扫描标记,关闭写屏障(Write Barrier),需 STW(stop the world)
  • 清理(Sweeping):将需要回收的内存归还到堆中,将过多的内存归还给操作系统,与用户程序并发执行

五、源码解析

见:

src/runtime/mgc.go

src/runtime/mbarrier.go

以下是简化后的核心源码及注释,聚焦混合写屏障的关键逻辑:

// 混合写屏障核心函数:当对象 ptr 新增引用到对象 val 时触发
// ptr:指向源对象的指针(如黑色对象 A 或灰色对象 C)
// val:被引用的目标对象(如白色对象 B)
func writeBarrier(ptr *uintptr, val uintptr) {
    // 1. 若目标对象 val 为空,或不在堆上,直接返回(不处理栈上对象)
    if val == 0 || !inHeap(val) {
        return
    }

    // 2. 若源对象 ptr 所在的 span 未被标记,直接返回
    ptrSpan := spanOf(ptr)
    if ptrSpan == nil || !ptrSpan.inMarking() {
        return
    }

    // 3. 获取目标对象 val 所在的 span 和标记状态
    valSpan := spanOf(val)
    valObjIdx := valSpan.objIndex(val) // 计算 val 在 span 中的索引
    valMarked := valSpan.gcmarkBits.get(valObjIdx) // 获取 val 的标记状态

    // 4. 混合写屏障核心逻辑:若目标对象是白色,标记为灰色
    if !valMarked {
        // 将 val 标记为灰色
        valSpan.gcmarkBits.set(valObjIdx)
        // 将 val 加入标记队列,后续遍历其引用的子对象
        queueForMarking(val)
    }

    // 5. 若源对象是栈上对象,记录需要最终 STW 扫描(优化点)
    if inStack(ptr) {
        stackScanNeeded = true
    }
}

// 辅助函数:判断地址是否在堆上
func inHeap(addr uintptr) bool {
    return addr >= heapStart && addr < heapEnd
}

// 辅助函数:判断地址是否在栈上
func inStack(addr uintptr) bool {
    // 遍历所有 Goroutine 的栈范围,判断 addr 是否在其中
    for _, g := range allGoroutines() {
        if addr >= g.stack.start && addr < g.stack.end {
            return true
        }
    }
    return false
}

核心逻辑拆解:

  1. 过滤非堆对象:栈上对象修改引用不触发写屏障(后续 STW 扫描),仅处理堆上对象;
  2. 状态检查:仅在标记阶段处理,且确保源对象和目标对象所在的 span 处于标记状态;
  3. 颜色修正:若目标对象是白色(未标记),将其标记为灰色并加入标记队列,避免漏标记;
  4. 栈扫描标记:若源对象是栈上对象,标记需要在标记终止阶段 STW 扫描栈,确保栈上对象引用的一致性。

六、观察 Go GC 行为

光看原理不够直观,我们通过一段实战代码,结合 Go 自带的工具观察 GC 行为,同时给出实际开发中的调优技巧。

(一)监控 GC 关键指标

package main

import (
    "fmt"
    "runtime"
    "time"
)

// 模拟内存分配:创建大量对象,触发 GC
func allocateMemory() {
    var slice []*int
    // 循环分配 100 万个 int 对象(每个 int 8 字节,共约 8 MB)
    for i := 0; i < 1000000; i++ {
        num := new(int)
        *num = i
        slice = append(slice, num)
    }
    // 保持引用,避免被提前回收
    _ = slice
}

func main() {
    // 禁用 GC 自动触发,手动控制(方便观察)
    runtime.GC() // 先执行一次 GC,清理初始垃圾
    runtime.DisableGC()

    fmt.Println("开始分配内存...")
    allocateMemory()
    fmt.Println("内存分配完成,准备触发 GC")

    // 记录 GC 开始时间
    start := time.Now()
    // 手动触发 GC
    runtime.EnableGC()
    runtime.GC()
    // 计算 GC 耗时
    gcDuration := time.Since(start)

    // 打印 GC 相关指标
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    fmt.Printf("GC 耗时:%v\n", gcDuration)
    fmt.Printf("已分配堆内存:%v MB\n", m.Alloc/1024/1024)
    fmt.Printf("GC 总次数:%d\n", m.NumGC)
    fmt.Printf("STW 总时间:%v\n", m.PauseTotal)
}

// 输出示例(不同环境可能有差异):
// 开始分配内存...
// 内存分配完成,准备触发 GC
// GC 耗时:1.234ms
// 已分配堆内存:8 MB
// GC 总次数:1
// STW 总时间:123.456µs

关键指标解读:

  • gcDuration:GC 总耗时(包含并发标记和 STW 时间);
  • m.Alloc:当前已分配的堆内存大小;
  • m.NumGC:程序运行以来的 GC 总次数;
  • m.PauseTotal:所有 STW 阶段的总耗时(可以看到混合写屏障下 STW 时间极短)。

(二)代码调优

通过代码调优,减少 GC 压力。

GC 调优的核心思路是“减少垃圾产生”和“优化内存分配”,结合 GMP 模型和 GC 机制,给出三个高频调优技巧:

避免大量小对象的频繁分配

小对象(如小于 16 字节)会被分配到“微对象分配器”,但频繁分配会导致大量 mspan 被占用,增加 GC 标记和清理的开销。

// 错误示例:频繁分配小对象
func badAlloc() {
    for i := 0; i < 1000000; i++ {
        // 每次循环分配一个 int 小对象
        s := fmt.Sprintf("num: %d", i)
        _ = s
    }
}

// 正确示例:复用对象或使用对象池
var strPool = sync.Pool{
    New: func() interface{} {
        b := make([]byte, 32)
        return &b
    },
}

func goodAlloc() {
    for i := 0; i < 1000000; i++ {
        // 从对象池获取复用对象
        bPtr := strPool.Get().(*[]byte)
        b := *bPtr
        // 重置并复用缓冲区
        b = b[:0]
        b = append(b, "num: "...)
        b = strconv.AppendInt(b, int64(i), 10)
        _ = string(b)
        // 归还对象到池
        strPool.Put(bPtr)
    }
}

原理:sync.Pool 可以复用临时对象,减少小对象的频繁分配和回收,降低 GC 标记和清理的压力。

合理设置 GOGC 环境变量

GOGC 是控制 GC 触发时机的环境变量,默认值为 100,代表当堆内存增长到上次 GC 后内存的 2 倍时触发 GC。

根据业务场景调整 GOGC 可优化 GC 频率:

  • CPU 密集型场景:可将 GOGC 调大(如 200),减少 GC 触发频率,降低 GC 对 CPU 的占用;

  • 内存敏感型场景:可将 GOGC 调小(如 50),提前触发 GC,避免内存占用过高。

使用方式:运行程序时指定 GOGC=200 ./your-program

避免内存逃逸到堆上

栈上的对象会随着 Goroutine 退出而自动释放,无需 GC 处理。若变量因“内存逃逸”被分配到堆上,会增加 GC 开销。

可通过 go build -gcflags="-m" 查看逃逸分析结果,优化代码避免不必要的逃逸。

// 错误示例:变量逃逸到堆上
func escapeDemo() *int {
    num := 10
    return &num // 返回栈上变量地址,导致逃逸到堆
}

// 正确示例:避免逃逸(根据业务场景调整返回方式)
func noEscapeDemo() int {
    num := 10
    return num // 返回值,不逃逸
}

常见问题

Q1. 混合写屏障为什么不需要扫描堆上对象,只需要扫描栈上对象?

因为堆上对象的引用修改会被混合写屏障监控:当堆上黑色/灰色对象修改引用指向白色对象时,白色对象会被标记为灰色,不会漏标。

而栈上对象的引用修改不触发写屏障(为了减少性能开销),所以必须在标记终止阶段短暂 STW,扫描所有栈上对象,确保栈上引用的对象都被正确标记。

Q2. gc 多久执行一次,什么时候触发

  • 定时触发:Go 运行时系统会根据一定的时间间隔定期触发垃圾回收。时间间隔根据程序的内存使用情况和性能需求进行自适应调整
  • 内存分配触发:当程序申请的内存超过一定阈值时,Go 运行时会触发垃圾回收,以防止过度使用内存
  • 栈伸缩触发:当 Goroutine 的栈空间不足以容纳当前的执行需要时,Go 运行时会触发垃圾回收来扩展栈空间
  • 主动触发:调用 runtime.GC
  • 空间不足时触发: 当前线程的内存管理单元中不存在空闲空间时,创建32KB以下的对象可能触发垃圾收集,创建32KB以上的对象时,一定会尝试触发

Q3. 为什么混合写屏障不保护栈的引用

因为go在并发运行时,大部分的操作都发生在栈上,函数调用会非常频繁。数十万goroutine的栈都进行屏障保护自然会有性能问题

虽然混合写屏障不保护栈上的引用,但 Go 语言的垃圾回收器在标记终止阶段会对栈进行重新扫描。在这个阶段,会暂停所有的用户程序(STW),对栈上的对象和引用进行精确的标记,确保所有可达对象都被正确标记。这样就弥补了不使用写屏障保护栈上引用的不足,保证了垃圾回收的正确性。

综上所述,混合写屏障不保护栈的引用是为了在保证垃圾回收正确性的前提下,尽可能提高程序的性能和降低实现复杂度。通过栈重新扫描机制,也能确保栈上的可达对象不会被错误回收。

Q4. gc 过程中那一部分使用了 STW

  • 标记准备阶段(Mark Setup)

    在这个阶段,垃圾回收器需要初始化标记状态,开启写屏障等操作。为了确保标记的正确性,需要暂停所有的用户程序,进行 STW

    在标记阶段开始前会进行一次STW,暂停所有goroutine的执行,然后再进行标记操作

  • 标记终止阶段(Mark Termination)

    在并发标记阶段结束后,可能还有一些标记工作没有完成,如一些新创建的对象或者修改的引用关系没有被标记。因此,需要暂停所有的用户程序,完成剩余的标记工作,关闭写屏障,统计所有需要回收的对象。

Q5. Go GC 的清理阶段是并发的,如何避免清理时 Goroutine 访问垃圾对象?

通过“写屏障”和“内存保护”实现:清理阶段,写屏障仍处于开启状态,当 Goroutine 访问对象时,会先检查该对象是否已被清理。若对象已被标记为垃圾且未清理,会等待清理完成;若已清理,会触发“空指针引用”错误(但实际中 GC 会确保存活对象不被清理,因为存活对象已被标记为黑色)。

Q6. 如何查看 Go 程序的 GC 详细日志?

通过设置环境变量 GODEBUG=gctrace=1 运行程序,会输出详细的 GC 日志,包含各阶段耗时、内存变化等信息。例如: GODEBUG=gctrace=1 ./your-program 日志中关键字段:pause 代表 STW 时间,alloc 代表堆内存分配大小,mark 代表并发标记耗时。

Q7. 高并发场景下,如何平衡 GC 性能和业务性能?

核心原则是“减少 GC 压力”:

  1. 复用对象:通过 sync.Pool 复用临时对象,减少垃圾产生;
  2. 控制内存增长:避免一次性分配大量内存,采用分批分配的方式;
  3. 调整 GOGC:根据业务是 CPU 密集还是内存密集,调整 GOGC 优化 GC 频率;
  4. 避免逃逸:通过逃逸分析优化代码,减少堆上内存分配。

总结

Go 的“三色标记 + 混合写屏障”GC 方案,核心是通过“高效标记”和“并发安全”的平衡,实现低延迟的垃圾回收。

其设计精髓可概括为三点:

  1. 三色标记提效:通过颜色区分对象状态,结合位图操作实现批量高效标记,降低标记阶段的时间开销;
  2. 混合写屏障保准:结合插入写屏障和删除写屏障的优势,监控堆上对象引用修改,仅需短暂 STW 扫描栈,解决并发标记的一致性问题;
  3. 并发执行降延迟:除标记准备和终止阶段的短暂 STW 外,标记和清理阶段均与 Goroutine 并发执行,将 GC 对业务的影响降到最低。

实际开发中,GC 调优的核心不是“调优 GC 本身”,而是“优化内存分配”——减少垃圾产生、避免不必要的堆分配,才能从根本上降低 GC 压力。

理解 GC 机制的底层逻辑,才能写出更高效、更稳定的 Go 并发程序。

如果大家关于 Go Gc 垃圾回收算法的解读还有哪些不清楚的地方,欢迎大家在评论区交流~~~

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-gc/

备用原文链接: https://blog.fiveyoboy.com/articles/go-gc/