Go 不存在“堆外内存”,mheap 管理全部用户态堆内存,其 arena_start~arena_end 明确划定堆地址边界,所有对象(含大对象、逃逸变量)均落在此范围内;mspan 是 mheap 下按页(8KB)组织、按 size class 划分的物理管理单元,通过 elemsize、freeindex 和 allocBits 将连续页切分为固定大小对象块,供分配使用。
Go 没有“堆外内存”这个概念;所谓“堆外”,其实是误读——mheap 管理的就是 Go 全部的用户态堆内存,而 mspan 是它最底层的物理管理单元,不存在独立于 mheap 的“堆外”。
为什么说 mheap 就是 Go 堆的全部边界
mheap 不是抽象容器,而是直接持有操作系统返回的虚拟地址空间(通过 mmap 或 brk)。它内部的 allspans 切片记录了每一个被申请过的 mspan,arena_start~arena_end 明确划定了整个堆的地址范围。任何 Go 对象(小对象、大对象、逃逸到堆的局部变量)最终都落在这个区间内。
常见误解是把 unsafe + syscall.Mmap 之类手动申请的内存当作“堆外”,但那属于用户自己管理的裸内存,Go 运行时既不感知、也不回收,和 mheap 完全无关。
-
mheap的freelarge和busylarge直接管理 >32KB 的大对象,它们也走mheap分配路径,不是“堆外” - 即使你用
runtime/debug.FreeOSMemory(),释放的也是mheap归还给操作系统的那部分页,不会动其他区域 -
pprof中的heap_inuse、heap_released统计全部来自mheap字段,没有第二套指标
mspan 是怎么把一页页内存变成可用对象块的
mspan 本质是一组连续的 8KB 页(npages ≥ 1),但它不直接暴露地址给用户代码。真正起作用的是它的三个关键字段:
立即学习“go语言免费学习笔记(深入)”;
-
elemsize:决定每个对象占多少字节(比如 class 4 是 48B),所有该mspan中的对象大小必须一致 -
freeindex:当前空闲槽位的下标,分配时递增,回收时重置(GC 清扫后) -
allocBits:位图,每个 bit 标记一个对象是否已分配,allocCache是它的局部缓存,加速查找
例如:一个 npages=1(即 8KB)、elemsize=32 的 mspan,最多容纳 8192 / 32 = 256 个对象,nelems=256,allocCount 达到 256 就变 full。
注意:spanclass 高位标识是否含指针(如 32-0 vs 32-1),这直接影响 GC 扫描行为——noscan 类型的 mspan 在标记阶段完全跳过。
go语言参考手册 中文CHM版
Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。本文给大家带来Go参考手册,需要的可以来下载! Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。 Go 语言特色 简洁、快速、安全 并行、有趣、开源 内存管理、v数组安全、编译
下载
什么时候会绕过 mcache 和 mcentral 直接碰 mheap
只有两种情况会直接调用 mheap 的分配逻辑(allocSpan):
- 分配 >32KB 的大对象:跳过所有缓存层级,直接从
mheap.freelarge或新 mmap 一块 arena,构造单个mspan并返回首地址 -
mcentral的nonempty链表为空,且自身也没有可用 span 时:它会锁住mheap.lock,从mheap.free或mheap.freelarge拿一个 span,初始化后塞进nonempty
这意味着:即使你只分配一个 33KB 的 []byte,也会触发全局 mheap.lock,成为并发瓶颈点。这也是为什么高吞吐服务要避免频繁分配大 buffer。
另外,mheap.grow 在内存不足时调用 sysReserve 向 OS 申请新 arena(64MB/块),这个过程本身不立即提交物理页(lazy commit),但地址空间已被划归 mheap 管辖。
调试时怎么看真实的 mspan 状态
运行时没有公开 API 直接遍历 mheap.allspans,但可通过以下方式观察:
- 启动时加
GODEBUG=madvdontneed=1,gctrace=1,GC 日志中每轮会打印scvg释放页数,背后就是mheap在操作mspan -
go tool pprof进入交互后用top查看runtime.mallocgc,再用web看调用链,能定位到nextFreeFast(查allocCache)或nextFree(查allocBits) - 用
runtime.ReadMemStats获取HeapObjects、HeapAlloc等,这些值全部由mheap的各链表状态累加而来,不是采样估算
真正容易被忽略的是:mspan 的 state 字段(如 mSpanInUse、mSpanManualScavenge)只在 runtime 内部原子更新,外部无法可靠读取;想确认某个地址是否属于某 mspan,唯一办法是用 runtime.spanOf(非导出),或依赖 pprof 的 symbolization 结果反推。