# 8.5 免清扫式位图技术
清扫过程非常简单,它与赋值器(用户代码)并发执行。 它的主要职能便是如何将一个已经从内存分配器中分配出得内存回收到内存分配器中。
## 启动方式
标记终止结束后,会进入`GCoff`阶段,并调用`gcSweep`来并发的使后台清扫器 Goroutine 与赋值器并发执行。
```
func gcMarkTermination(nextTriggerRatio float64) {
...
systemstack(func() {
...
// 标记阶段已经完成,关闭写屏障,开始并发清扫
setGCPhase(_GCoff)
gcSweep(work.mode)
})
...
}
```
其实现非常简单,只需要将 mheap\_ 相关的标志位清零,并唤醒后台清扫器 Goroutine 即可。
```
//go:systemstack
func gcSweep(mode gcMode) { // 此时为 GCoff 阶段
...
lock(&mheap_.lock)
mheap_.sweepgen += 2
mheap_.sweepdone = 0
...
mheap_.pagesSwept = 0
mheap_.sweepArenas = mheap_.allArenas
mheap_.reclaimIndex = 0
mheap_.reclaimCredit = 0
unlock(&mheap_.lock)
// 出于调试目的,用户可以让 sweep 过程阻塞执行,但我们并不感兴趣
...
// 并发清扫(唤醒后台 Goroutine)
lock(&sweep.lock)
if sweep.parked {
sweep.parked = false
ready(sweep.g, 0, true)
}
unlock(&sweep.lock)
}
```
## 并发清扫
清扫过程依赖下面的结构:
```
var sweep sweepdata
type sweepdata struct {
lock mutex
g *g
parked bool
started bool
nbgsweep uint32
npausesweep uint32
}
```
该结构通过:
1. mutex 保证清扫过程的原子性
2. g 指针来保存所在的 Goroutine
3. started 判断是否开始
4. nbgsweep 和 npausesweep 来统计清扫过程
当一个后台 sweeper 从应用程序启动时休眠后,再重新唤醒时,会进入如下循环,并一直在次循环中反复休眠与被唤醒:
```
func bgsweep(c chan int) {
...
for {
// 清扫 span,如果清扫了一部分 span,则记录 bgsweep 的次数
for sweepone() != ^uintptr(0) {
sweep.nbgsweep++
Gosched()
}
// 可抢占的释放一些 workbufs 到堆中
for freeSomeWbufs(true) {
Gosched()
}
// 在 mheap_ 上判断是否完成清扫,若未完成,则继续进行清扫
lock(&sweep.lock)
if !isSweepDone() { // 即 mheap_.sweepdone != 0
unlock(&sweep.lock)
continue
}
// 否则让 Goroutine 进行 park
sweep.parked = true
goparkunlock(&sweep.lock, waitReasonGCSweepWait, traceEvGoBlock, 1)
}
}
```
sweepone 从堆中清理
```
func sweepone() uintptr {
_g_ := getg()
...
// 增加锁的数量确保 Goroutine 在 sweep 中不会被抢占,进而不会将 span 留到下个 GC 产生不一致
_g_.m.locks++
if atomic.Load(&mheap_.sweepdone) != 0 {
_g_.m.locks--
return ^uintptr(0)
}
// 记录 sweeper 的数量
atomic.Xadd(&mheap_.sweepers, +1)
// 寻找需要 sweep 的 span
var s *mspan
sg := mheap_.sweepgen
for {
s = mheap_.sweepSpans[1-sg/2%2].pop()
if s == nil {
atomic.Store(&mheap_.sweepdone, 1)
break
}
if s.state != mSpanInUse {
...
continue
}
if s.sweepgen == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
break
}
}
// sweep 找到的 span
npages := ^uintptr(0)
if s != nil {
npages = s.npages
if s.sweep(false) { // false 表示将其归还到 heap 中
// 整个 span 都已被释放,记录释放的额度,因为整个页都能用作 span 分配了
atomic.Xadduintptr(&mheap_.reclaimCredit, npages)
} else {
// span 还在被使用,因此返回零
// 并需要 span 移动到已经 sweep 的 in-use 列表中。
npages = 0
}
}
// 减少 sweeper 的数量并确保最后一个运行的 sweeper 正常标记了 mheap.sweepdone
if atomic.Xadd(&mheap_.sweepers, -1) == 0 && atomic.Load(&mheap_.sweepdone) != 0 {
...
}
_g_.m.locks--
return npages
}
```
```
// freeSomeWbufs 释放一些 workbufs 回到堆中,如果需要再次调用则返回 true
func freeSomeWbufs(preemptible bool) bool {
const batchSize = 64 // 每个 span 需要 ~1–2 µs
lock(&work.wbufSpans.lock)
// 如果此时在标记阶段、或者 wbufSpans 为空,则不需要进行释放
// 因为标记阶段 workbufs 需要被标记,而 workbufs 为空则更不需要释放
if gcphase != _GCoff || work.wbufSpans.free.isEmpty() {
unlock(&work.wbufSpans.lock)
return false
}
systemstack(func() {
gp := getg().m.curg
// 清扫一批 span,64 个,大约 ~1–2 µs
// 在需要被抢占时停止、在清扫完毕后停止
for i := 0; i < batchSize && !(preemptible && gp.preempt); i++ {
span := work.wbufSpans.free.first
if span == nil {
break
}
// 将 span 移除 wbufSpans 的空闲链表中
work.wbufSpans.free.remove(span)
// 将 span 归还到 mheap 中
mheap_.freeManual(span, &memstats.gc_sys)
}
})
// workbufs 的空闲 span 列表尚未清空,还需要更多清扫
more := !work.wbufSpans.free.isEmpty()
unlock(&work.wbufSpans.lock)
return more
}
```
```
//go:systemstack
func (h *mheap) freeManual(s *mspan, stat *uint64) {
s.needzero = 1 // span 在下次被分配走时需要对该段内存进行清零
lock(&h.lock)
*stat -= uint64(s.npages << _PageShift)
memstats.heap_sys += uint64(s.npages << _PageShift) // 记录并增加堆中的剩余空间
h.freeSpanLocked(s, false, true) // 将其释放会堆中
unlock(&h.lock)
}
func (h *mheap) freeSpanLocked(s *mspan, acctinuse, acctidle bool) {
switch s.state {
case mSpanManual:
... // panic
case mSpanInUse:
...
h.pagesInUse -= uint64(s.npages)
// 清除 arena page bitmap 正在使用的二进制位
arena, pageIdx, pageMask := pageIndexOf(s.base())
arena.pageInUse[pageIdx] &^= pageMask
default:
... // panic
}
if acctinuse {
memstats.heap_inuse -= uint64(s.npages << _PageShift)
}
if acctidle {
memstats.heap_idle += uint64(s.npages << _PageShift)
}
s.state = mSpanFree
// 与邻居进行结合
h.coalesce(s)
// 插入回 treap
h.free.insert(s)
}
```
- 第一部分 :基础篇
- 第1章 Go语言的前世今生
- 1.2 Go语言综述
- 1.3 顺序进程通讯
- 1.4 Plan9汇编语言
- 第2章 程序生命周期
- 2.1 从go命令谈起
- 2.2 Go程序编译流程
- 2.3 Go 程序启动引导
- 2.4 主Goroutine的生与死
- 第3 章 语言核心
- 3.1 数组.切片与字符串
- 3.2 散列表
- 3.3 函数调用
- 3.4 延迟语句
- 3.5 恐慌与恢复内建函数
- 3.6 通信原语
- 3.7 接口
- 3.8 运行时类型系统
- 3.9 类型别名
- 3.10 进一步阅读的参考文献
- 第4章 错误
- 4.1 问题的演化
- 4.2 错误值检查
- 4.3 错误格式与上下文
- 4.4 错误语义
- 4.5 错误处理的未来
- 4.6 进一步阅读的参考文献
- 第5章 同步模式
- 5.1 共享内存式同步模式
- 5.2 互斥锁
- 5.3 原子操作
- 5.4 条件变量
- 5.5 同步组
- 5.6 缓存池
- 5.7 并发安全散列表
- 5.8 上下文
- 5.9 内存一致模型
- 5.10 进一步阅读的文献参考
- 第二部分 运行时篇
- 第6章 并发调度
- 6.1 随机调度的基本概念
- 6.2 工作窃取式调度
- 6.3 MPG模型与并发调度单
- 6.4 调度循环
- 6.5 线程管理
- 6.6 信号处理机制
- 6.7 执行栈管理
- 6.8 协作与抢占
- 6.9 系统监控
- 6.10 网络轮询器
- 6.11 计时器
- 6.12 非均匀访存下的调度模型
- 6.13 进一步阅读的参考文献
- 第7章 内存分配
- 7.1 设计原则
- 7.2 组件
- 7.3 初始化
- 7.4 大对象分配
- 7.5 小对象分配
- 7.6 微对象分配
- 7.7 页分配器
- 7.8 内存统计
- 第8章 垃圾回收
- 8.1 垃圾回收的基本想法
- 8.2 写屏幕技术
- 8.3 调步模型与强弱触发边界
- 8.4 扫描标记与标记辅助
- 8.5 免清扫式位图技术
- 8.6 前进保障与终止检测
- 8.7 安全点分析
- 8.8 分代假设与代际回收
- 8.9 请求假设与实务制导回收
- 8.10 终结器
- 8.11 过去,现在与未来
- 8.12 垃圾回收统一理论
- 8.13 进一步阅读的参考文献
- 第三部分 工具链篇
- 第9章 代码分析
- 9.1 死锁检测
- 9.2 竞争检测
- 9.3 性能追踪
- 9.4 代码测试
- 9.5 基准测试
- 9.6 运行时统计量
- 9.7 语言服务协议
- 第10章 依赖管理
- 10.1 依赖管理的难点
- 10.2 语义化版本管理
- 10.3 最小版本选择算法
- 10.4 Vgo 与dep之争
- 第12章 泛型
- 12.1 泛型设计的演进
- 12.2 基于合约的泛型
- 12.3 类型检查技术
- 12.4 泛型的未来
- 12.5 进一步阅读的的参考文献
- 第13章 编译技术
- 13.1 词法与文法
- 13.2 中间表示
- 13.3 优化器
- 13.4 指针检查器
- 13.5 逃逸分析
- 13.6 自举
- 13.7 链接器
- 13.8 汇编器
- 13.9 调用规约
- 13.10 cgo与系统调用
- 结束语: Go去向何方?