字符串底层字节数组走 tiny allocator 需同时满足:长度 ≤16 字节且无指针(即 byte 数组),此时通过 mallocgc(len, nil, false) 分配,因 noscan 且尺寸小而进入 tiny 路径;否则按 small(≤32kb)或 large(>32kb)路径分配。

Go 语言中字符串本身不直接参与 mallocgc 分配决策,真正被分配的是其底层 data 字段指向的字节切片([]byte)所占用的堆内存。字符串是只读结构体,分配行为完全由底层数组长度决定。
字符串底层数据何时走 tiny allocator?
字符串内容实际存储在 data 字段,类型为 *byte;分配时走的是 mallocgc(len, nil, false)(noscan=true,因为 byte 数组无指针)。所以是否进入 tiny allocator,取决于两个条件同时满足:
len (即 <code>maxTinySize)- 分配请求标记为
noscan(字符串底层数组确实满足)
例如:str := "abc" → 底层分配 3 字节 → 进入 tiny allocator,合并进一个 16B slot;但 str := string(make([]byte, 17)) → 17 字节 → 跳过 tiny,落入小对象路径(size class 24B)。
为什么 len==16 的字符串不一定复用同一个 tiny slot?
tiny allocator 只对 noscan 且 ≤16B 的请求合并,但它不区分语义,只按地址连续性“填空”。关键限制在于:
- tiny slot 是 per-P 的
mcache.tiny指针,每次分配返回的是同一 slot 内偏移地址 - 一旦当前 slot 填满(或被其他协程抢占),就申请新 slot,旧 slot 不会被回收重用
- 不同 P 上的 tiny 分配互不干扰,不存在跨 P 共享
所以即使两个字符串都是 8 字节,若发生在不同 P 或不同时间点,大概率落在不同 tiny slot 中 —— 这不是 bug,而是无锁设计的代价。
大字符串(>32KB)直接 mmap,但有隐含页对齐开销
当字符串底层数组长度 > maxSmallSize(32768 字节),mallocgc 会跳过所有缓存层级,直接调用 mheap.allocSpan 从操作系统申请整数页(page size = 8KB)。注意:
- 实际分配页数 =
(len + _PageSize - 1) / _PageSize,比如 32769 字节 → 5 页(40KB) - 这些 span 不经过 mcentral/mcache,也不参与 GC 标记(因 noscan),但依然计入
HeapSys和HeapInuse - 不会立即归还 OS:scavenger 默认 5 分钟空闲后才 munmap,且仅对完全空闲的 span 生效
这意味着一个临时构造的 1MB 字符串,即使很快被丢弃,其对应 span 在 5 分钟内仍占 RSS,pprof::heap 看不到它,但 ps aux 会体现。
字符串拼接引发的隐式分配容易踩坑
看似简单的 s := a + b + c 在编译期未必优化,运行时可能触发多次分配:
- 若总长 ≤16B 且全为常量 → 静态分配,无 mallocgc
- 否则走
runtime.concatstrings,内部调用mallocgc分配目标大小的新底层数组 - 尤其注意
fmt.Sprintf、strconv.Itoa等函数返回字符串,其长度不可控,可能意外落入大对象路径
更隐蔽的是 string([]byte{...}) 强制转换:如果源 slice 容量远大于长度,底层数据仍被完整复制,分配大小等于 slice 长度,而非 cap —— 但开发者常误以为“只是转类型”,忽略分配成本。
真正难调试的不是阈值数字本身,而是 tiny allocator 的不可预测复用、noscan 大对象不归还 OS 的滞后性,以及拼接类操作在逃逸分析失效时的多级分配放大效应。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











