Engineering · 2 min read
分配器內部:size class、fragmentation、scavenger
size class 是速度和碎片的平衡。Go 分配器怎麼把 page 切成 slot、碎片從哪來、為什麼 pprof 掉了 RSS 沒跟著掉。
size class 是速度和碎片的平衡,分配快,但位置不一定剛好。
四層配置階梯
- mcache(per-P):每個 P 持有各 size class 的 span cache。大多數配置在這裡完成,無鎖、無 syscall(GMP 排程器與 netpoller)。
- Size classes(8B, 16B, 24B, 32B⋯):10-byte 物件放 16B slot。同尺寸在同 span → 空出的 slot 立刻能被同 class 重用。
- Bitmap free-slot search:每個 span 用 bitmap 追蹤 slot 狀態。找空位 = 找最低 set bit,O(1) per word。
碎片:內部和外部
- Internal:size-class 向上取整(17B → 24B)。Go 的 67 個 size class 控制浪費 < 12%。struct field 排列也影響:
{int8, int64, int8}= 24B,{int64, int8, int8}= 16B。 - External:散落的活物件卡住整個 span,空間存在但無法歸還。Go 用 non-move GC,低 CPU 成本,但不壓縮。
Scavenger 與 madvise
- 背景 scavenger 定期透過
madvise歸還 idle pages。MADV_DONTNEED(Go ≥1.16 預設)= 立即歸還,RSS 掉,但重用要 page fault。MADV_FREE(Go <1.16)= 懶回收,RSS 看起來黏住。 - Go 1.16 改回
MADV_DONTNEED的原因:太多「RSS 怎麼不掉」的 false alarm。
在這個 OOM 裡的角色
這次 OOM 不是分配器碎片造成的,是瀏覽器記憶體。但理解分配器能解釋為什麼「pprof 的 inuse 掉了,RSS 沒掉」,因為分配器拿了記憶體不一定馬上還(記憶體三層模型)。
分配器是 kernel 和 heap 之間的中間人。不理解它,RSS 和 pprof 之間的差距就永遠對不起來。
參考來源:
- https://go.dev/src/runtime/malloc.go
- https://go.dev/src/runtime/mheap.go
- https://go.dev/src/runtime/sizeclasses.go
- https://go.dev/ref/spec#Size_and_alignment_guarantees
- https://go-review.googlesource.com/c/go/+/267100
- https://man7.org/linux/man-pages/man2/madvise.2.html
Related: 參見記憶體三層模型、OOM 診斷決策樹、GMP 排程器與 netpoller,或回到系列總覽。