Go 原理之 gc 垃圾回收机制:三色标记 + 混合写屏障(需要 STW)
用过 Go 开发的都知道,它的垃圾回收(GC)机制是保障程序稳定运行的关键——尤其是在高并发场景下,GC 的延迟直接影响服务的响应性能。
Go 1.5 版本后,GC 机制逐步演进为“三色标记 + 混合写屏障”的组合方案,将 STW(Stop The World,世界暂停)时间压缩到毫秒级甚至微秒级。
今天,我们从原理到源码,彻底讲透这套 GC 方案的设计逻辑与实现细节。
一、常见垃圾回收算法
在学习 Go Gc 垃圾回收算法之前,我们先对现有的比较常见的垃圾回收算法做个大概了解,如下表:
| 垃圾回收算法 | 描述 | 代表语言 | 优缺点 |
|---|---|---|---|
| 引用计数 | 为每个对象维护一个引用计数,记录对象被引用的次数 每当一个对象被引用时,引用计数就会增加。 当对象不再被引用时,引用计数就会减少。 如果对象的引用计数变为 0, 则对象可以被垃圾回收器回收 |
Python、PHP | 优点: 实现简单,处理快 缺点: 无法处理循环引用,两个对象相互引用,计数永远不为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 在标记过程中修改了对象的引用),可能导致“漏标记”或“误标记”。
为解决并发标记的一致性问题,业界有两种经典方案:
- 写屏障:监控对象引用的修改,当修改发生时标记相关对象,保证标记一致性;
- 快照:在标记开始时生成对象引用的快照,基于快照进行标记,但会占用额外内存。
Go 选择了写屏障方案,并结合“三色标记”算法提升标记效率,最终在 Go 1.8 版本确定了“三色标记 + 混合写屏障”的成熟方案——既实现了大部分阶段的并发执行,又通过混合写屏障解决了传统写屏障的不足,将 STW 时间控制在极低水平。
三、三色标记与混合写屏障是什么?
要理解 Go GC 的核心逻辑,必须先吃透“三色标记”和“混合写屏障”这两个核心概念。
它们一个负责“高效标记垃圾”,一个负责“保证并发标记的一致性”,相辅相成。
(一)三色标记
三色标记:高效区分“存活对象”与“垃圾”
三色标记算法是一种基于“对象可达性”的标记方法,通过给对象标记三种颜色,区分不同状态的对象,实现高效的并发标记。
三种颜色对应对象的三种状态,核心逻辑围绕“可达性分析”展开——从根对象(如 Goroutine 栈上的变量、全局变量)出发,遍历所有可达对象,未被遍历到的就是垃圾。
三种颜色的定义及流转逻辑:
| 颜色 | 状态定义 | 处理逻辑 |
|---|---|---|
| 白色 | 未被标记的对象 | 初始状态,所有对象都是白色;标记结束后仍为白色的是垃圾,将被清理 |
| 灰色 | 已被标记,但引用的子对象未完全标记 | 处于“待处理”状态,会被放入标记队列,后续需遍历其引用的子对象 |
| 黑色 | 已被标记,且引用的子对象已全部标记 | 处于“已完成”状态,不会再被遍历;黑色对象是存活对象,标记阶段不会被清理 |
核心流转流程:
- 初始时,所有对象为白色,根对象(如 Goroutine 栈变量、全局变量)被标记为灰色,放入标记队列;
- 从标记队列取出灰色对象,遍历其引用的子对象:将白色子对象标记为灰色并加入队列,然后将当前灰色对象标记为黑色;
- 重复步骤 2,直到标记队列为空;
- 标记结束,白色对象即为垃圾。
源码中对象标记的核心结构体(简化版,来自 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
三色标记-流程
-
- 初始时,所有对象被标记为白色
-
- gc 开始,遍历 rootset 根节点,将有引用对象的对象标记为 灰色,存入灰色对象列表
-
- 遍历 灰色对象,将直接可达对象标记为 灰色,并将自身标记为 黑色
-
- 重复第3步,直到标记完所有的对象 (灰色对象列表为空)
-
- 将白色对象清除,保留黑色对象
如下图:
(二)混合写屏障
混合写屏障:解决并发标记的一致性问题。
三色标记在并发执行时,最大的问题是“对象引用被动态修改”。
比如:黑色对象 A 原本引用白色对象 B,在标记过程中,A 突然取消引用 B,而灰色对象 C 新增引用 B——此时 B 会被漏标记,最终被误判为垃圾。
为解决这个问题,需要通过“写屏障”监控对象引用的修改。
Go 之前使用过“插入写屏障”和“删除写屏障”,但各有不足:
- 插入写屏障:当有新引用插入时(如 C 引用 B),将 B 标记为灰色,但会导致栈上对象需要最终 STW 重新扫描;
- 删除写屏障:当有引用删除时(如 A 取消引用 B),将 B 标记为灰色,但会导致大量不必要的标记。
Go 1.8 引入的“混合写屏障”结合了两者的优势,核心规则(四句话总结):
- 写屏障开启时,所有新分配的对象直接标记为黑色;
- 当灰色对象或黑色对象修改引用,指向白色对象时,将白色对象标记为灰色;
- 当栈上对象修改引用时,不触发写屏障(栈上对象最终会短暂 STW 扫描);
- 堆上对象修改引用时,触发写屏障。
混合写屏障的优势:既避免了插入写屏障对栈的频繁扫描,又减少了删除写屏障的冗余标记,同时保证了并发标记的一致性,大幅缩短 STW 时间。
大白话讲解:
三色标记存在并发问题:
在三色标记期间,如果没有STW,并发创建对象,可能存在垃圾对象或误删对象的情况:
-
-
黑色对象的引用对象被删除,则不可达,正常黑色对象应该被回收,但是gc期间只会循环遍历灰色列表,不会回收黑色对象,因此该对象为垃圾对象(多余垃圾对象)。eg:对象1已经被标记为黑色,表示该对象有引用方,受保护,如果没有stw,该对象的引用可能被删除,正常应该转为白色对象被清除,然gc并不会清除黑色对象。
-
-
黑色对象引用了白色对象,白色对象有了引用对象应该被保护,但仍然被无情的回收(清掉不该清的对象)。
白色对象只有被灰色对象引用情况,才会判断是否需要清理,白色对象如果在gc期间引用了黑色对象,那只会被误删除。
所以 go 引入了 混合写屏障 机制,满足:
- 强三色不变式:黑色对象不允许引用了白色对象;因为一旦引用,该黑色对象将不会继续参与 gc,白色对象会被无理清除。
- 弱三色不变式:黑色对象可以引用白色对象,但该白色对象必须被其它灰色对象或其上游有灰色对象引用,否则该白色对象将被无理清除。
这里需要注意一点,插入屏障仅会在堆内存中生效,不对栈内存空间生效,这是因为 go 在并发运行时,大部分的操作都发生在栈上,函数调用会非常频繁。
数十万 goroutine 的栈都进行屏障保护自然会有性能问题
所以 gc 期间,任何在栈上新创建的对象,均为黑色。
| 混合写屏障 | 开启期间 | 描述 |
|---|---|---|
| 插入写屏障 | 创建的新对象为灰色对象 | 满足:强三色不变式。 不会存在黑色对象引用白色对象 |
| 删除写屏障 | 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色 | 满足:弱三色不变式 (保护灰色对象到白色对象的路径不会断) |
(三)STW
STW:不可避免的“短暂暂停”。
虽然“并发标记”是核心,但 Go GC 仍需要短暂的 STW 阶段——主要用于“标记准备”和“标记终止”,原因是这两个阶段需要操作全局状态,无法并发执行:
- 标记准备:暂停所有 Goroutine,初始化标记状态(如重置位图、构建根对象列表),时间通常在微秒级;
- 标记终止:暂停所有 Goroutine,处理剩余的灰色对象,清理标记状态,时间通常在微秒级。
混合写屏障的引入,让栈上对象无需在并发标记阶段扫描,只需在标记终止前短暂 STW 扫描一次,这是 Go GC 能将 STW 时间压缩到极低水平的关键。
(四)优缺点
优点:
减少stw时间,三色标记需要stw整个程序,混合写屏障(分段stw)可以有效降低stw的时间
缺点:
回收精度低,有些垃圾需要在下一轮 gc 清理
四、Go GC 完整流程
三色标记 + 混合写屏障如何协同工作?
理解了核心概念后,我们结合 Go 1.21 版本的 GC 流程,拆解“三色标记 + 混合写屏障”的完整协同逻辑。
整个流程分为四个阶段,其中只有两个阶段需要短暂 STW。
阶段 1:标记准备(STW)
此阶段会暂停所有 Goroutine(STW),执行初始化操作,耗时极短:
- 暂停所有 Goroutine,禁止其修改对象引用;
- 初始化 GC 相关全局状态,如重置
gcmarkBits位图; - 构建根对象列表(根对象包括:全局变量、Goroutine 栈上的变量、寄存器中的变量);
- 将根对象标记为灰色,加入标记队列;
- 开启混合写屏障;
- 恢复所有 Goroutine,结束 STW。
阶段 2:并发标记(与 Goroutine 并发)
此阶段是 GC 的核心阶段,与 Goroutine 并发执行,无需 STW:
- 从标记队列中取出灰色对象,遍历其引用的所有子对象;
- 对每个子对象,通过
gcmarkBits检查状态:若为白色,标记为灰色并加入标记队列; - 遍历完成后,将当前灰色对象标记为黑色;
- 同时,混合写屏障监控堆上对象的引用修改:
- 新分配的堆对象直接标记为黑色;
- 堆上对象修改引用时,若指向白色对象,将其标记为灰色;
- 当标记队列中的灰色对象为空时,并发标记阶段结束。
阶段 3:标记终止(STW)
此阶段再次短暂 STW,处理并发标记的收尾工作:
- 暂停所有 Goroutine;
- 关闭混合写屏障;
- 扫描所有 Goroutine 的栈(因为栈上对象修改不触发写屏障,需最终扫描确保无漏标);
- 处理剩余的灰色对象(遍历其引用并标记);
- 计算垃圾对象的数量和大小,为清理阶段做准备;
- 恢复所有 Goroutine,结束 STW。
阶段 4:清理阶段(并发)
此阶段与 Goroutine 并发执行,清理被标记为白色的垃圾对象:
- 遍历所有
mspan,通过gcmarkBits识别白色对象; - 回收白色对象的内存,将其标记为“空闲”状态,存入对应大小等级的空闲链表;
- 对内存碎片进行整理(如合并相邻的空闲
mspan); - 清理完成后,重置
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
}核心逻辑拆解:
- 过滤非堆对象:栈上对象修改引用不触发写屏障(后续 STW 扫描),仅处理堆上对象;
- 状态检查:仅在标记阶段处理,且确保源对象和目标对象所在的
span处于标记状态; - 颜色修正:若目标对象是白色(未标记),将其标记为灰色并加入标记队列,避免漏标记;
- 栈扫描标记:若源对象是栈上对象,标记需要在标记终止阶段 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 压力”:
- 复用对象:通过
sync.Pool复用临时对象,减少垃圾产生; - 控制内存增长:避免一次性分配大量内存,采用分批分配的方式;
- 调整 GOGC:根据业务是 CPU 密集还是内存密集,调整 GOGC 优化 GC 频率;
- 避免逃逸:通过逃逸分析优化代码,减少堆上内存分配。
总结
Go 的“三色标记 + 混合写屏障”GC 方案,核心是通过“高效标记”和“并发安全”的平衡,实现低延迟的垃圾回收。
其设计精髓可概括为三点:
- 三色标记提效:通过颜色区分对象状态,结合位图操作实现批量高效标记,降低标记阶段的时间开销;
- 混合写屏障保准:结合插入写屏障和删除写屏障的优势,监控堆上对象引用修改,仅需短暂 STW 扫描栈,解决并发标记的一致性问题;
- 并发执行降延迟:除标记准备和终止阶段的短暂 STW 外,标记和清理阶段均与 Goroutine 并发执行,将 GC 对业务的影响降到最低。
实际开发中,GC 调优的核心不是“调优 GC 本身”,而是“优化内存分配”——减少垃圾产生、避免不必要的堆分配,才能从根本上降低 GC 压力。
理解 GC 机制的底层逻辑,才能写出更高效、更稳定的 Go 并发程序。
如果大家关于 Go Gc 垃圾回收算法的解读还有哪些不清楚的地方,欢迎大家在评论区交流~~~
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!