Go内存管理器Mspan
一 Go运行时简述
1.1 Go Runtime简介
Go语言的内存分配是自主管理的,所以内置了运行时(Runtime),这样能自主实现内存使用模式,如内存池、预分配等。这样的好处是不会让每次内存分配都进行系统调用(会从用户态切换到内核态)。
Golang的运行时内存分配算法基于TCMalloc算法,即Thread-Caching Malloc,其核心思想是把内存分为多级管理,降低了锁的粒度。在Go中,可用的堆内存采用二级 分配的方式进行管理。
Go中的每个线程都会自行维护一个独立的内存池,进行内存分配是会优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。
1.2 内存分配过程
Go程序在启动时会从操作系统申请一大块内存(可以减少系统调用,所以Go在刚启动时占用很大)。实际中,申请到的大块内存并不一定是连续的,Go会将这些零散的内存构建为一个链表,如图所示:
![] (runtime-07.svg)
mspan结构体即链表中的节点对象,位于 src/sruntime/mheap.go:
type mspan struct {
next *mspan // 双向链表下一个节点
prev *mspan // 双向链表前一个节点
startAddr uintptr // 起始序号
npages uintptr // 当前管理的页数
manualFreeList gclinkptr // 待分配的 object 链表
nelems uintptr // 剩余可分配块个数
allocCount uint16 // 已分配块个数
}
启动后申请到的内存在Go中会被重新分配虚拟地址空间,在X64上分别是 512MB、16GB、512GB,如图所示:
![]
(runtime-08.svg)
图中的三块区域:
- arena:即堆区,Go在这里进行动态内存分配,该区域被分割成了每块8KB大小的页Page,这些页组合成为 mspan
- bitmap:表示页中具体的信息,即arena区哪些地址保存了对象,bitmap使用4bit标志位表示对象是否包含指针、GC标记信息
- spans区域:表示具体页,即mspan指针,每个指针对应一页,spans区域的大小即为:
- 512GB/8KB:得到arena区域的页数
- 上述结构*8B:得到spans区域所有指针大小,其值为512MB
源码位于:src/runtime/malloc.go
_PageShift = 13
_PageSize = 1 << _PageShift // 1左移13 (1后面有13个0) 8KB
注意:内存分配器只负责内存块的创建、提取等,其回收动作是由GC清理后触发的,不会主动回收!
内存分配器会将管理的内存分为两种:
- span:由多个连续的页组成
- object:span会被按照特定大小切分成多个小块,每个小块都可以用于存储对象
具体的分配过程:
- 为对象分配内存时,只需要从链表中取出一个大小合适的节点即可
- 为对象回收内存时,会将对象使用的内存重新插回到链表中
- 如果闲置内存过多,也会尝试归还部分内存给操作系统,降低整体开销