编写过C或者C++程序的都知道,我们要手动申请或释放内存。Go的内存是自动管理的,不需要我们考虑内存的申请和释放问题。尽管我们不需要考虑内存的管理问题,但了解Go在内存管理方法做了什么,有助于我们写出高效的程序。

先来一张Go内存分配的全局图

tD7gc6.png

分配方法

顺序分配器

当我们在编程语言中使用顺序分配器,我们只需要在内存中维护一个指向内存特定位置的指针,当用户程序申请内存时,分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置,即移动下图中的指针

t4asSK.png

顺序分配器的问题是:无法在内存被释放时重用内存。如下图所示,如果已经分配的内存被回收,顺序分配器是无法重新利用红色的这部分内存的:

t4dEkR.png

所以顺序分配器在回收内存时,需要整理内存碎片,将空闲内存定期合并。

空闲链表分配器

Go使用的内存分配器,将内存分割成多个链表,每个链表中的内存块大小相同,申请内存时先找到满足条件的链表,再遍历链表,找到空闲的内存块。(时间复杂度O(n))。

t4wN5R.png

这种方法可以避免顺序分配器的内存碎片,同时减少了需要遍历的内存块数量。

具体实现

内存基本单元

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存储的对象大小个数

classbytes/objbytes/spanobjectswaste bytes
18819210240
21681925120
33281922560
...............
66327683276810
  • 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列表
}

tIuf78.png

当为小于32k的对象分配内存时,go会从运行当前协程的P上的线程缓存mcache,根据对象的大小,找到对应的mspan列表,遍历列表,为对象分配内存。

mcahce的存在,避免了多线程申请内存时需要加锁。

中心缓存

当为对象分配内存时,如果线程缓存mcache对应的mspan列表已经没有空闲的内存块可以分配,会向中心缓存mcentral申请内存。

tIjALq.png

Go为每一种class类型的mspan维护一个mcentral,每一个mcentral都有一个empty listnoempty list

tIOJj1.png

type mcentral struct {
    lock      mutex     //互斥锁
    spanclass spanClass // span class ID
    nonempty  mSpanList // non-empty 指还有空闲块的span列表
    empty     mSpanList // 指没有空闲块的span列表

    nmalloc uint64      // 已累计分配的对象个数
}

mcentral不同于mcachemcache是作为线程的私有资源,为单个线程服务的,而mcentral是全局资源,服务多个线程,访问中心缓存中的内存管理单元需要使用互斥锁

页堆

mcentral的数据结构可见,每个mcentral对象只管理特定的class类型的mspan

堆上初始化的所有对象都由mheap统一管理,页堆mheap包含一个长度为67*2的mcentral列表。

tIxIoV.png

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数组管理所有的内存。

tIxHWF.png

type heapArena struct {
    bitmap [heapArenaBitmapBytes]byte
    spans [pagesPerArena]*mspan
    pageInUse [pagesPerArena / 8]uint8
    pageMarks [pagesPerArena / 8]uint8
    zeroedBase uintptr
}

mcentral没有足够的内存时,会向mheap申请。

tIzeTP.png

大对象

超过32K的对象,会直接被分配到堆上。

tIzX9S.png

参考资料

内存分配原理

内存分配器

Understanding Allocations: the Stack and the Heap - GopherCon SG 2019

Last modification:June 10th, 2020 at 09:19 am
如果觉得我的文章对你有用,请尽情赞赏 🐶