Go 内存分配器的设计与实现( 四 )


Go 内存分配器的设计与实现

文章插图
 
go-memory-layout
图 7-10 Go 程序的内存布局
所有的 Go 语言程序都会在启动时初始化如上图所示的内存布局,每一个处理器都会被分配一个线程缓存 runtime.mcache 用于处理微对象和小对象的分配,它们会持有内存管理单元 runtime.mspan 。
每个类型的内存管理单元都会管理特定大小的对象,当内存管理单元中不存在空闲对象时,它们会从 runtime.mheap 持有的 134 个中心缓存 runtime.mcentral 中获取新的内存单元,中心缓存属于全局的堆结构体 runtime.mheap,它会从操作系统中申请内存 。
在 amd64 的 Linux 操作系统上,runtime.mheap 会持有 4,194,304 runtime.heapArena,每一个 runtime.heapArena 都会管理 64MB 的内存,单个 Go 语言程序的内存上限也就是 256TB 。
内存管理单元runtime.mspan 是 Go 语言内存管理的基本单元,该结构体中包含 next 和 prev 两个字段,它们分别指向了前一个和后一个 runtime.mspan:
type mspan struct { next *mspan prev *mspan ...}串联后的上述结构体会构成如下双向链表,运行时会使用 runtime.mSpanList 存储双向链表的头结点和尾节点并在线程缓存以及中心缓存中使用 。
Go 内存分配器的设计与实现

文章插图
 
【Go 内存分配器的设计与实现】mspan-and-linked-list
图 7-11 内存管理单元与双向链表
因为相邻的管理单元会互相引用,所以我们可以从任意一个结构体访问双向链表中的其他节点 。
页和内存每个 runtime.mspan 都管理 npages 个大小为 8KB 的页,这里的页不是操作系统中的内存页,它们是操作系统内存页的整数倍,该结构体会使用下面的这些字段来管理内存页的分配和回收:
type mspan struct { startAddr uintptr // 起始地址 npagesuintptr // 页数 freeindex uintptr allocBits*gcBits gcmarkBits *gcBits allocCache uint64 ...}
  • startAddr 和 npages — 确定该结构体管理的多个页所在的内存,每个页的大小都是 8KB;
  • freeindex — 扫描页中空闲对象的初始索引;
  • allocBits 和 gcmarkBits — 分别用于标记内存的占用和回收情况;
  • allocCache — allocBits 的补码,可以用于快速查找内存中未被使用的内存;
runtime.mspan 会以两种不同的视角看待管理的内存,当结构体管理的内存不足时,运行时会以页为单位向堆申请内存:
Go 内存分配器的设计与实现

文章插图
 
mspan-and-pages
图 7-12 内存管理单元与页
当用户程序或者线程向 runtime.mspan 申请内存时,该结构会使用 allocCache 字段以对象为单位在管理的内存中快速查找待分配的空间:
Go 内存分配器的设计与实现

文章插图
 
mspan-and-objects
图 7-13 内存管理单元与对象
如果我们能在内存中找到空闲的内存单元,就会直接返回,当内存中不包含空闲的内存时,上一级的组件 runtime.mcache 可能会为该结构体添加更多的内存页以满足为更多对象分配内存的需求 。
状态运行时会使用 runtime.mSpanStateBox 结构体存储内存管理单元的状态runtime.mSpanState:
type mspan struct { ... statemSpanStateBox ...}该状态可能处于 mSpanDead、mSpanInUse、mSpanManual 和 mSpanFree 四种情况 。当 runtime.mspan 在空闲堆中,它会处于 mSpanFree 状态;当 runtime.mspan 已经被分配时,它会处于 mSpanInUse、mSpanManual 状态,这些状态会在遵循以下规则发生转换:
  • 在垃圾回收的任意阶段,可能从 mSpanFree 转换到 mSpanInUse 和 mSpanManual;
  • 在垃圾回收的清除阶段,可能从 mSpanInUse 和 mSpanManual 转换到 mSpanFree;
  • 在垃圾回收的标记阶段,不能从 mSpanInUse 和 mSpanManual 转换到 mSpanFree;
设置 runtime.mspan 结构体状态的读写操作必须是原子性的避免垃圾回收造成的线程竞争问题 。
跨度类runtime.spanClass 是 runtime.mspan 结构体的跨度类,它决定了内存管理单元中存储的对象大小和个数:
type mspan struct { ... spanclassspanClass ...}Go 语言的内存管理模块中一共包含 67 种跨度类,每一个跨度类都会存储特定大小的对象并且包含特定数量的页数以及对象,所有的数据都会被预选计算好并存储在 runtime.class_to_size 和
runtime.class_to_allocnpages 等变量中:
classbytes/objbytes/spanobjectstail wastemax waste1881921024087.50%2168192512043.75%3328192256046.88%44881921703231.52%5648192128023.44%68081921023219.07%..................6632768327681012.50%


推荐阅读