0

Go内存分配

 2 years ago
source link: https://doumao.cc/index.php/%E7%BC%96%E7%A8%8B/Go%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

classbytes/objbytes/spanobjectswaste bytes188192102402168192512033281922560...............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列表
}

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

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

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

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

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列表。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK