mcache 必须绑定到 p 而非 g 或 m,因 p 是稳定独占的调度单元;mcentral 按 spanclass 分锁,共 134 个实例;mcache 归还由后台协程异步触发,单位为 span,gc 不清理 mcache。

Go 的 mcache 和 mcentral 不是“并列组件”,而是明确的上下级供给关系:一个 mcache 从对应 mcentral 拿内存,不是反过来。
为什么 mcache 必须绑定到 P,而不是 G 或 M?
因为 goroutine(G)频繁调度、可能跨 M 迁移,而操作系统线程(M)数量不稳定;只有 P 是稳定、独占、且与调度强绑定的运行时单元。把 mcache 放在 P 上,才能保证:同一组 goroutine 在多数情况下复用本地缓存,又避免锁竞争。
常见错误现象:mcache 被误认为属于 goroutine,导致在 runtime.Gosched() 或 channel 阻塞后误判缓存归属;实际上只要 G 还在同一个 P 上运行,它就继续使用该 P 的 mcache。
-
mcache生命周期 = P 的生命周期,P 被销毁时其mcache中未归还的 span 会批量交还给mcentral - 没有“全局 mcache”或“跨 P 共享 mcache”的设计;强行绕过 P 绑定会破坏无锁前提
- GC 扫描时不会遍历
mcache中已分配但未写入指针的 slot——这是mcache仅缓存“空闲块”而非“活跃对象”的关键体现
mcentral 的锁粒度到底有多细?
mcentral 的锁不是全局一把,而是按 spanClass 分离:每个 size class + scan/noscan 组合对应独立的 mcentral 实例。也就是说,分配 16B 有指针对象和分配 24B 无指针对象,完全不争抢同一把锁。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
典型使用场景:高并发服务中大量创建 struct{a int; b string}(约 32B),和大量 [8]byte(8B,noscan)同时发生——这两类分配走的是两个完全隔离的 mcentral,零锁冲突。
- Go 当前(1.22+)共定义 67 个 size class,每个分 scan/noscan,总计 134 个
mcentral实例 -
mcentral内部维护partial(含空闲块的 span)和full(无空闲块的 span)两个链表,只在操作partial时加锁 - 如果某个 size class 长期高频分配,
mcentral.partial可能变空,触发向mheap申请新 span——这时才暴露真正的瓶颈点
什么时候 mcache 会主动归还内存给 mcentral?
不是等 mcache “满了”才还,而是由 runtime 后台协程周期性扫描:当某个 size class 的缓存块数超过阈值(如 128 个),且最近未被访问,则触发归还。这个行为不可手动触发,也不响应 GC 周期。
容易踩的坑:以为手动调用 runtime.GC() 能清空 mcache——其实 GC 不动 mcache,它只清理堆上对象和 mcentral.full 中的 span;mcache 归还是异步、懒惰、基于统计的。
- 归还单位是整个 span,不是单个对象;所以即使只用了 span 中 1 个 slot,只要整 span 空闲且超龄,就整块退回
- 归还过程不阻塞分配;
mcache在归还途中仍可继续服务新请求,只是后续不再往该 span 放新对象 - 若程序长期只分配某几种 size class(比如全是 48B 结构体),其他 class 的
mcache条目可能长期闲置,但不会被强制释放——这是空间换时间的显式取舍
真正难调试的点不在结构本身,而在 span 复用边界:比如一个 map[int]int 底层 bucket 对象大小刚好卡在两个 size class 分界线上,微小字段增减会导致分配路径从 mcache 直接跳转到 mheap,性能毛刺由此产生。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










