GC
并发标记-清除(Concurrent Mark-Sweep):Go 的 GC 在大部分阶段与用户程序并发执行(非完全 STW)。
三色标记法:基于 Dijkstra 的三色标记算法(黑、灰、白对象),结合写屏障(Write Barrier)实现并发标记。
- 白色:初始状态,表示未被访问的对象(可能为垃圾)。
- 灰色:已被访问,但其引用的子对象还未被扫描。
- 黑色:已被访问,且其所有子对象也已被扫描(存活对象)。
Tri-Color Marking
(1) 初始化阶段(STW)
- 所有对象初始化为白色。
- 暂停程序(STW),扫描根对象(栈、全局变量、寄存器等),将其标记为灰色,加入待处理队列。
(2) 并发标记阶段
- 恢复程序运行,后台启动并发标记线程。
- 循环处理灰色对象:
- 从队列中取出一个灰色对象。
- 扫描该对象的所有引用,将其引用的白色子对象标记为灰色,加入队列。
- 将该对象自身标记为黑色。
- 此过程与用户程序并发执行,需依赖写屏障处理并发修改。
(3) 标记终止阶段(STW)
- 再次暂停程序,完成剩余的少量灰色对象处理。
- 确保所有存活对象均被标记为黑色,白色对象即为垃圾。
(4) 并发清除阶段
- 回收所有白色对象的内存,无需 STW。
Write Barrier
在并发标记过程中,用户程序可能修改对象引用关系,导致两种问题:
- 漏标:黑色对象新引用了白色对象,但黑色对象不再被扫描,导致白色对象被错误回收。
- 误标:灰色对象断开了对白色对象的引用,但白色对象被错误保留。
Go 通过混合写屏障(Hybrid Barrier)解决这些问题:
写屏障规则:
- 插入写屏障(Dijkstra Barrier):
- 当黑色对象
A
引用白色对象B
时,将B
标记为灰色。
- 当黑色对象
- 删除写屏障(Yuasa Barrier):
- 当灰色对象
A
断开对白色对象B
的引用时,将B
标记为灰色。
- 当灰色对象
GMP
- G(Goroutine):轻量级用户态线程,由 Go 运行时管理,栈大小动态伸缩(初始约 2KB)。
- M(Machine):操作系统线程(OS Thread),真正执行 CPU 指令的实体。
- P(Processor):逻辑处理器,负责调度 G 到 M 上运行,维护本地运行队列(Local Queue)。
协作关系:
- M 必须绑定 P 才能运行 G:每个 M 必须持有一个 P,通过 P 获取 G 并执行。
- P 的数量默认等于 CPU 核心数:由
GOMAXPROCS
控制(可动态调整)。 - G 的调度由 P 管理:每个 P 维护一个本地队列(Local Queue),同时存在一个全局队列(Global Queue)。
调度流程:
(1) 创建 Gorotinue:
- 新创建的 G 优先放入当前 P 的本地队列。
- 若本地队列已满,则将一部分 G 转移到全局队列。
(2) 获取可运行的 G:
- 本地队列优先:P 从本地队列获取 G,依次执行(队列头部出队)。
- 全局队列补充:若本地队列为空,从全局队列获取一批 G 到本地队列。
- 工作窃取(Work Stealing):若全局队列也为空,随机从其他 P 的本地队列窃取半数 G。
(3) 执行 G:
- M 绑定 P 后,循环从 P 的本地队列获取 G 并执行。
- 若 G 发生系统调用(如文件 I/O),Go 运行时将:
- M 与 P 解绑:P 被释放,以便其他 M 绑定并执行其他 G。
- M 阻塞等待系统调用完成:完成后,G 重新加入队列,M 尝试获取空闲 P 或进入休眠。
(4) 自旋线程:
- 若 M 未找到可运行的 G,会进入自旋状态(短暂循环检查队列),避免立即休眠以减少延迟。
- 自旋的 M 数量有限(通常为活跃 P 的数量),防止 CPU 空转。
Channel
- 无缓冲 channel:
- 发送和接收操作必须同时就绪(即发送会阻塞,直到有接收方;接收也会阻塞,直到有发送方)。
- 天然支持同步(常用于 Goroutine 间同步)。
- 缓冲 channel:
- 缓冲区未满时,发送非阻塞;缓冲区为空时,接收阻塞。
- 适用于生产者和消费者解耦的场景(异步通信)。
底层结构:
Go 的 Channel 由 runtime.hchan
结构体实现(简化后):
type hchan struct {
qcount uint // 当前队列中元素数量
dataqsiz uint // 缓冲区容量
buf unsafe.Pointer // 环形缓冲区指针
elemsize uint16 // 元素大小
closed uint32 // 是否已关闭
sendx uint // 发送索引(缓冲区的写入位置)
recvx uint // 接收索引(缓冲区的读取位置)
recvq waitq // 接收等待队列(因接收阻塞的 Goroutine)
sendq waitq // 发送等待队列(因发送阻塞的 Goroutine)
lock mutex // 互斥锁(保证原子操作)
}
机制:
- 等待队列:
- 当发送或接收操作无法立即完成时,Goroutine 会被加入
sendq
或recvq
队列,等待被唤醒。
- 当发送或接收操作无法立即完成时,Goroutine 会被加入
- 环形缓冲区:
- 缓冲通道使用环形队列存储数据,通过
sendx
和recvx
管理读写位置。
- 缓冲通道使用环形队列存储数据,通过
- 锁机制:
- 所有 Channel 操作均需加锁,保证并发安全。
注意事项:
(1) deadlock:
- 所有 Goroutine 均阻塞在 Channel 操作上。
(2) panic:
- 向已关闭的通道发送数据:触发
panic: send on closed channel
。 - 重复关闭通道:触发
panic: close of closed channel
。
(3) nil:
- 未初始化的 Channel 为零值(nil)。
(4) close:
- 若不再使用但未关闭,可能导致 Goroutine 阻塞或资源无法释放。
Map
map
是一种基于哈希表实现的键值对数据结构,提供高效的查找、插入和删除操作。
基本用法:
- 声明与初始化
// 声明一个 map(此时为 nil,不可直接操作)
// map[K]V, K 必须支持 == 操作(如 slice、map、func 不可作为键)
var m1 map[string]int
// 初始化空 map(推荐使用 make)
m2 := make(map[string]int)
// 初始化空 map 并预分配空间减少扩容次数
m3 := make(map[string]int, 10)
// 初始化并赋值
m4 := map[string]int{
"apple": 5,
"banana": 3,
}
- 增删改查
// 插入或更新
m3["orange"] = 2
// 读取
count := m3["apple"] // 存在时返回对应值,不存在返回 V 的默认值
count, ok := m3["pear"] // ok 表示是否存在
// 删除
delete(m3, "banana")
// 遍历(顺序随机) map 遍历会打乱顺序
for key, value := range m3 {
fmt.Println(key, value)
}
nil map:未初始化的 map(如 var m map[string]int
)为 nil
,插入数据会 panic。
底层实现:
type hmap struct {
count int // 当前元素数量
B uint8 // 桶数量的对数(桶数为 2^B)
buckets unsafe.Pointer // 桶数组指针
oldbuckets unsafe.Pointer // 扩容时旧桶数组
flags uint8
// ... 其他字段(哈希种子、溢出桶等)
}
哈希桶(Bucket)
- 每个桶(
bmap
)存储 8 个键值对。 - 哈希冲突时,通过链表法将溢出桶链接到主桶。
扩容机制
- 触发条件:
- 装载因子超过 6.5(元素数 / 桶数)。
- 溢出桶过多(表明哈希冲突严重)。
- 渐进式扩容:扩容时逐步迁移数据到新桶,避免一次性性能抖动。
Tips
- 遍历时删除元素:遍历过程中增删元素可能导致未定义行为(如部分元素未被遍历),删除当前遍历的元素不会引发错误。
- 内存管理:键或值为大对象,删除元素后需显式置为
nil
(或使用弱引用)。type BigStruct struct { Data [1e6]byte }
m := make(map[int]*BigStruct)
m[1] = &BigStruct{}
delete(m, 1) // 删除键,但 BigStruct 对象仍在堆中
m[1] = nil // 显式释放内存(若需回收) - 并发:map 本身不支持并发,可以加锁或使用
sync.Map
。
Slice && Array
定义与声明:
特性 | Array | Slice |
---|---|---|
长度固定性 | 长度固定,声明时指定(如 [3]int )。 | 长度动态可变,无需声明长度(如 []int )。 |
类型系统 | 长度是类型的一部分([2]int 和 [3]int 不同)。 | 类型与长度无关([]int 是统一类型)。 |
初始化方式 | 显式指定长度或由编译器推断([...]int{1,2,3} )。 | 直接初始化或基于数组/切片创建(如 s := make([]int, 0, 5) )。 |
内存分配与存储:
特性 | Array | Slice |
---|---|---|
内存分配 | 连续内存块,通常分配在栈(小数组)或堆。 | 引用类型,包含指针、长度、容量,底层依赖数组,通常在堆。 |
传递方式 | 值传递:赋值或传参时复制整个数组。 | 引用传递:传递切片头(指针+长度+容量),共享底层数组。 |
底层共享 | 无共享,每个数组独立。 | 多个切片可共享同一底层数组,修改可能相互影响。 |
Comments NOTHING