编写过C或者C++程序的都知道,我们要手动
申请或释放内存。Go的内存是自动
管理的,不需要我们考虑内存的申请和释放问题。尽管我们不需要考虑内存的管理问题,但了解Go在内存管理方法做了什么,有助于我们写出高效的程序。
先来一张Go内存分配的全局图
分配方法
顺序分配器
当我们在编程语言中使用顺序分配器,我们只需要在内存中维护一个指向内存特定位置的指针,当用户程序申请内存时,分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置,即移动下图中的指针
顺序分配器的问题是:无法在内存被释放时重用内存
。如下图所示,如果已经分配的内存被回收,顺序分配器是无法重新利用红色的这部分内存的:
所以顺序分配器在回收内存时,需要整理内存碎片,将空闲内存定期合并。
空闲链表分配器
Go使用的内存分配器,将内存分割成多个链表,每个链表中的内存块大小相同
,申请内存时先找到满足条件的链表,再遍历链表,找到空闲的内存块。(时间复杂度O(n))。
这种方法可以避免顺序分配器的内存碎片,同时减少了需要遍历的内存块数量。
具体实现
内存基本单元
mspan
是Go内存管理的基本单元。
type mspan struct {
next *mspan //链表前向指针,用于将span链接起来
prev *mspan //链表前向指针,用于将span链接起来
startAddr uintptr // 起始地址,也即所管理页的地址
npages uintptr // 管理的页数
nelems uintptr // 块个数,也即有多少个块可供分配
allocBits *gcBits //分配位图,每一位代表一个块是否已分配
allocCount uint16 // 已分配块的个数
spanclass spanClass // class表中的class ID
elemsize uintptr // class表中的对象大小,也即块大小
}
Go根据对象大小,划分为一系列class,每个class代表一个固定大小的对象,和每个mspan
的大小。上面mspan
结构体里的spanClass
属性,它决定了一个内存管理单元mspan
存储的对象大小
和个数
。
class | bytes/obj | bytes/span | objects | waste bytes |
---|---|---|---|---|
1 | 8 | 8192 | 1024 | 0 |
2 | 16 | 8192 | 512 | 0 |
3 | 32 | 8192 | 256 | 0 |
... | ... | ... | ... | ... |
66 | 32768 | 32768 | 1 | 0 |
- bytes/obj:该class代表的对象占用的字节数
- bytes/span:每个内存管理单元占用的字节数
- objects:每个内存管理单元可以分配的对象个数,objects = (bytes/span) / (bytes/obj)
- waste bytes:每个内存管理单元产生的内存碎片,waste bytes = (bytes/span) % (bytes/obj)
所有一个内存管理单元mspan
一般是可以分配多个对象的,内存管理单元和对象一般是一对多
的关系。
线程缓存
mcache
是管理mspan
的数据结构。mcache
(线程缓存)会与处理器P一一绑定。每一个线程缓存都会有67*2个mspan
列表。
列表中每个元素代表一种class类型的mspan
列表,每种class类型都有两组span列表,第一组列表中所表示的对象中包含了指针,第二组列表中所表示的对象不含有指针,这么做是为了提高GC扫描性能,对于不包含指针的span列表,没必要去扫描。
type mcache struct {
alloc [67*2]*mspan // 按class分组的mspan列表
}
当为小于32k的对象分配内存时,go会从运行当前协程的P上的线程缓存mcache
,根据对象的大小,找到对应的mspan
列表,遍历列表,为对象分配内存。
mcahce
的存在,避免了多线程申请内存时需要加锁。
中心缓存
当为对象分配内存时,如果线程缓存mcache
对应的mspan
列表已经没有空闲的内存块可以分配,会向中心缓存mcentral
申请内存。
Go为每一种class类型的mspan
维护一个mcentral
,每一个mcentral
都有一个empty list
和noempty list
。
type mcentral struct {
lock mutex //互斥锁
spanclass spanClass // span class ID
nonempty mSpanList // non-empty 指还有空闲块的span列表
empty mSpanList // 指没有空闲块的span列表
nmalloc uint64 // 已累计分配的对象个数
}
mcentral
不同于mcache
,mcache
是作为线程的私有资源,为单个线程服务的,而mcentral
是全局资源,服务多个线程,访问中心缓存中的内存管理单元需要使用互斥锁
。
页堆
从mcentral
的数据结构可见,每个mcentral
对象只管理特定的class类型的mspan
。
堆上初始化的所有对象都由mheap
统一管理,页堆mheap
包含一个长度为67*2的mcentral
列表。
type mheap struct {
lock mutex
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
central [134]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
mheap
通过使用二维的heapArena
数组管理所有的内存。
type heapArena struct {
bitmap [heapArenaBitmapBytes]byte
spans [pagesPerArena]*mspan
pageInUse [pagesPerArena / 8]uint8
pageMarks [pagesPerArena / 8]uint8
zeroedBase uintptr
}
当mcentral
没有足够的内存时,会向mheap
申请。
大对象
超过32K的对象,会直接被分配到堆上。
参考资料
Understanding Allocations: the Stack and the Heap - GopherCon SG 2019