go小对象按8/16/32字节等分档,因mcache和mcentral按size class预分配固定尺寸span,避免碎片并加速分配;结构体字段顺序、指针逃逸、sync.pool复用方式共同决定实际落档。

Go 小对象为什么按 8 字节、16 字节、32 字节分档?
因为 Go 的 mcache 和 mcentral 按 size class 管理小对象内存,不是按需切分,而是预分配固定尺寸的 span。每个 size class 对应一个大小区间(比如 17–32 字节的对象统一走 class 4,分配 32 字节),目的是避免碎片 + 加速分配。
常见错误现象:runtime.MemStats.AllocBytes 明明只申请了 25 字节,但 runtime.ReadMemStats 显示实际多占了 7 字节;或者压测时发现大量 tiny allocs 占用高,其实是 tiny allocator 在合并小字段(如多个 byte 或 bool)进同一个 16 字节槽位,反而引发 false sharing。
- 结构体字段顺序直接影响 size class:把
int64放前面、byte放后面,可能让整体从 24 字节(class 6)掉到 32 字节(class 7);反过来调整,可能压进 24 字节档 -
unsafe.Sizeof返回的是“理论最小”,但runtime.GC统计的AllocBytes是按实际分配的 size class 计的 - 小于 16 字节的对象(如
struct{a byte; b bool})大概率进tiny allocator,不单独走 size class,但会和其他 tiny 对象拼进同一个 16 字节块 —— 这意味着 GC 扫描时哪怕只改一个字段,整个 16 字节块都算 live
怎么查自己代码里某个 struct 落在哪个 size class?
别猜,用 go tool compile -gcflags="-m -l" 看逃逸分析的同时,配合 runtime/debug.ReadGCStats 或 pprof heap profile 观察分配量级;更直接的是查 Go 源码里的 src/runtime/sizeclasses.go,它硬编码了全部 67 个 class 的上限值(如 class 0=8B, class 1=16B, ..., class 15=32KB)。
使用场景:你想优化高频创建的 RequestCtx 或 Item 结构体,但不确定改字段顺序有没有效果。
立即学习“go语言免费学习笔记(深入)”;
- 用
unsafe.Sizeof(T{})得到字节数后,在sizeclasses.go里二分查找“第一个 ≥ 该值”的 class 上限 —— 那就是它实际分配的档位 - 注意:如果结构体含指针,且逃逸到堆上,才走这套 size class;栈上分配不经过 mcache,也不受此约束
- Go 1.22+ 引入了新的 class 划分(比如新增 class 64 处理更大对象),旧版工具链可能误判,建议用当前项目 Go 版本对应的源码对照
为什么给 struct 补齐 padding 有时反而更省内存?
不是为了对齐 CPU,是为了对齐 size class 边界。比如一个 struct 实际 25 字节,补齐到 32 字节后,虽然单个变大,但可能让它从 class 7(32B)稳定下来 —— 而原来 25 字节会落入 class 7(上限 32B),但若字段排列导致编译器插入 7 字节 padding,总大小还是 32B;但如果没显式补齐,后续加字段容易跨档升到 class 8(48B),涨 50%。
性能影响:补 padding 减少跨 class 升档,降低 mcache miss 概率;但过多 padding 会提高 cache line 冗余度,尤其在 slice of struct 场景下。
- 推荐做法:用
github.com/alexflint/go-sizes工具跑sizes.StructLayout,看 padding 分布和当前 class - 不要盲目追求“零 padding”——有些 3 字节 struct 补 5 字节变 8 字节,比让它卡在 16 字节 class 更划算
- 含
[3]byte和int64的 struct,若int64在前,[3]byte在后,总大小是 16 字节(8+3+5);反过来就变成 24 字节(3+5+8+8),直接跳档
sync.Pool 里放小对象,size class 还重要吗?
仍然重要,而且更隐蔽。因为 sync.Pool 的本地池(poolLocal)里存的是 interface{},底层仍走 malloc → size class 分配;Put/Get 不改变对象原始分配尺寸,只是复用。
容易踩的坑:以为用了 sync.Pool 就不用管大小,结果发现 Get 出来的对象每次都要重新触发 mcache 分配 —— 其实是因为 Put 前对象被修改过,导致 GC 认为它不可复用,或 Pool 自动清理时丢弃了部分 span。
- 确保 Put 前重置所有字段(包括指针置 nil),否则 runtime 可能拒绝回收进 pool,转而走常规分配路径
- Pool 中对象若来自不同 size class(比如混着 16B 和 24B 的 struct),会导致 mcache 中多个 class 的 span 都被 hold 住,增加内存驻留
- 高频 Get/Put 小对象时,观察
/debug/pprof/heap?debug=1中 “stacks” 是否显示大量runtime.mallocgc调用 —— 如果有,说明 pool 复用率低,size class 不稳定可能是诱因之一
真正难的不是算清每个 class 的边界,而是意识到:字段顺序、是否指针、逃逸与否、Pool 使用方式,这四件事拧在一起,才决定最终落在哪一档。改一行字段位置,可能让 QPS 提两个点,也可能让 RSS 涨 15% —— 没有银弹,只有实测。










