# 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去向何方?