如何轻松理解Golang 中的内存管理?

Golang 内存结构

Go在程序启动的时候,会先向操作系统申请一块内存(注意这时还只是一段虚拟的地址空间,并不会真正地分配内存),切成小块后自己进行管理。

arena:

对象基本上被分配在arena这个区域

Page:8KB(以页为单位)

 

bitmap:

bitmap区域标识arena区域哪些地址保存了对象,并且用4bit标志位表示对象是否包含指针、GC标记信息。bitmap中一个byte大小的内存对应arena区域中4个指针大小(指针大小为 8B )的内存,所以bitmap区域的大小是512GB/(4*8)=16GB,相当于1B对应4*8B。通过bitmap把页分的更小了,提高后续内存回收效率和解决内存碎片化的问题。
 
Spans:
这个区域存储mSpan指针
mSpan:由若干Page组成,相当于管理这些Page的,一个mspan可以管理一个Page也可以同时管理多个Page。按照最大占用空间,一个Span管理一个Page,(512GB/ 8KB)*8B=512MB(最大范围,因为每个指针大小8B,一个指针最小对应一个页)
mspan具体的结构
 
type mspan struct {
    //链表前向指针,用于将span链接起来
    next *mspan  
    //链表前向指针,用于将span链接起来
    prev *mspan   
    // 起始地址,也即所管理页的地址
    startAddr uintptr     
    // 管理的页数
    npages uintptr 
    // 块个数,表示有多少个块可供分配
    nelems uintptr 
    //分配位图,每一位代表一个块是否已分配
    allocBits *gcBits 
    // 已分配块的个数
    allocCount uint16   
    // class表中的class ID,和Size Classs相关 Size_Classs = Span_Classs / 2
    spanclass spanClass  
    // class表中的对象大小,也即块大小
    elemsize uintptr 
}

mspan再分配的小单元的大小就是根据Size_Classs来划分的

const _NumSizeClasses = 67
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536,1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

mspan所能分到的页数也是根据Size_Classs来划分的

const _NumSizeClasses = 67
var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}

拿上图举例:

npages =1 标示分配了一个Page
spanclass = 10 => sizeclass = 5 => 查询一个object大小elemsize = class_to_size[10] = 144B
nelems = 8KB/144B = 56.88 = 56,有一些内存浪费掉了
allocBits就是标记分配块的分配情况。(在每个mspan管理的所有地址可能继续被分成小的单元,根据对象的大小,将这块也分为更多小的块)
mspan具体的结构体(mspan是一个包含起始地址、mspan规格、页的数量等内容的双端链表。)

Golang内存分配:

mcache(每个工作线程都会绑定一个mcache),不同的线程之间不存在竞争,不需要消耗额外的锁资源。

type mcache struct {
    alloc [numSpanClasses]*mspan
}
numSpanClasses = _NumSizeClasses << 1 //span class 数量是 size class 数量的两倍

67 * 2 = 134 ncache实际一共拥有134种span class ,其作用为了加速内存回收的速度,数组里一半的mspan中分配的对象不包含指针,另一半则包含指针,对于无指针对象的mspan在进行垃圾回收的时候无需进一步扫描它是否引用了其他活跃的对象。

size class 到 span class的计算如下:

// noscan为true代表对象不包含指针
func makeSpanClass(sizeclass uint8, noscan bool) spanClass {
       return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
}        

mcentral(管理全局的mspan供所有线程使用,存在锁竞争的问题),上图把mcentral归为一个,实际有134个mcentral,每个mcentral管理自己mspan

mcentral:为所有mcache提供切分好的mspan资源。每个central保存一种特定大小的全局mspan列表,包括已分配出去的和未分配出去的。 每个mcentral对应一种mspan,而mspan的种类导致它分割的object大小不同。当工作线程的mcache中没有合适(也就是特定大小的)的mspan时就会从mcentral获取。mcentral 和mcache一样,也有0~133这134个span class级别,但每个级别都保存了两个span list:

1. noempty:有空闲对象的mspan列表。这些span是mcache释放span时加入这个链表的。

2. empty:链表里的mspan都被分配了object,或者是已经被cache取走了的mspan,这个mspan就被那个工作线程独占了。

mcentral被所有的工作线程共同享有,存在多个Goroutine竞争的情况,因此会消耗锁资源。结构体定义:

type mcentral struct {
    // 互斥锁
    lock mutex  
    // 规格
    sizeclass int32  
    // 尚有空闲object的mspan链表
    nonempty mSpanList  
    // 没有空闲object的mspan链表,或者是已被mcache取走的msapn链表
    empty mSpanList  
    // 已累计分配的对象个数
    nmalloc uint64 
}

mheap(管理Go的所有动态分配内存)

mheap:代表Go程序持有的所有堆空间,Go程序使用一个mheap的全局对象_mheap来管理堆内存。

mcentral没有空闲的mspan时,会向mheap申请。而mheap没有资源时,会向操作系统申请新内存。mheap主要用于大对象的内存分配,以及管理未切割的mspan,用于给mcentral切割成小对象。mheap中含有所有规格的mcentral,所以,当一个mcachemcentral申请mspan时,只需要在独立的mcentral中使用锁,并不会影响申请其他规格的mspan

当分配一个对象的时候,为它寻找合适的mspan:

  1. 计算对象所需内存大小size
  2. 根据size到size class映射,计算出所需的size class
  3. 根据size class和对象是否包含指针计算出span class
  4. 获取该span class指向的span。

具体如下:

  1.<=16B 的对象使用mcache的tiny分配器分配;

  2.(16B,32KB] 的对象,首先计算对象的规格大小,然后使用mcache中相应规格大小的mspan分配;

    如果mcache没有相应规格大小的mspan,则向mcentral申请

    如果mcentral没有相应规格大小的mspan,则向mheap申请

    如果mheap中也没有合适大小的mspan,则向操作系统申请

  3.>32KB大对象,使用mheap直接分配,若mheap没有足够的内存,则mheap向虚拟内存申请若干个pages

Golang内存管理的优点

   1. mspan的递增结构便于减少产生内存碎片。申请内存的时候是以span为单位的,span又分为不同大小,从大小的规律可以看到不是简单的按照2次幂进行递增的,是根据计算造成碎片最少的情况下对span的分类,在申请的时候会减少内存碎片。比如在申请47B大小的时候,如果按照2次幂会提供64B大小的内存供应用使用,但是如果按照span会提供48B大小的span,很明显看出,后者造成的碎片会更少。

  2. golang在程序启动时会申请一块内存由自己分配,避免了系统调用

  3. mspan设计了包含指针的区域和不包含指针的区域,以空间换时间,提高了内存回收速度

  4. go的内存算法是使用google的TCMalloc内存管理算法,把内存分的非常细,分为多级管理,减少锁的粒度。在回收对象内存时,并没有将其真正释放掉,只是放回预先分配的大块内存中,以便复用。只有内存闲置过多的时候,才会尝试归还部分内存给操作系统,降低整体开销

posted @ 2019-11-22 19:54  LeeJuly  阅读(495)  评论(0)    收藏  举报