直接回答:Go 的分配器脱胎于 TCMalloc,核心是三级缓存加按大小分级。小对象(≤32KB)按约 70 个 size class 规格化分配:每个 P 挂一个 mcache(无锁,因为 P 同一时刻只跑一个 goroutine),mcache 缺货向 mcentral(按 size class 分桶,带锁)批量领 span,mcentral 也空了才找 mheap 按页(8KB)要,mheap 再不够才向 OS 申请。大对象(>32KB)绕过前两级直接找 mheap。tiny 分配(<16B 无指针)还有专门的合并装箱。

展开解析:span 是基本管理单元(若干连续页,同 size class 的对象槽位),bitmap 标记哪些槽空闲、哪些是指针(GC 扫描用)。这个结构解释了多个现象:mcache 按 P 分配而非按 M,是 Go 1.1 的优化,避免 P 数远小于 M 时的浪费;堆内存“只涨不跌”的老印象源于早期版本激进持有 span,现代版本后台 scavenger 会逐步把空闲页返还 OS(madvise), RSS 回落慢不等于泄漏;span 内部碎片——规格化意味着 17 字节的对象也占 32 字节槽,大量近似尺寸对象时浪费可观。逃逸分析决定走栈还是堆,堆分配的成本大头不在分配本身而在后续 GC 压力,pprof 的 alloc_space 与 inuse_space 要分开看。调优手段很少(GOGC、ballast),正道是减少分配:sync.Pool、预分配、避免 fmt.Sprintf 这类隐藏分配。

追问方向:为什么 mcache 挂在 P 而不是 M 上?内存如何归还 OS?

(约 490 字)