zoukankan      html  css  js  c++  java
  • golang内存分配

      Go语言内置运行时(就是runtime),不同于传统的内存分配方式,go为自主管理,最开始是基于tcmalloc架构,后面逐步迭新。自主管理可实现更好的内存使用模式,如内存池、预分配等,从而避免了系统调用所带来的性能问题。

    1. 基本策略

    • 每次从操作系统申请一大块内存,然后将其按特定大小分成小块,构成链表(组织方式是一个单链表数组,数组的每个元素是一个单链表,链表中的每个元素具有相同的大小。);
    • 为对象分配内存时从大小合适的链表提取一小块,避免每次都向操作系统申请内存,减少系统调用。
    • 回收对象内存时将该小块重新归还到原链表,以便复用;若闲置内存过多,则归还部分内存到操作系统,降低整体开销。

    1.1 内存块

      span:即上面所说的操作系统分配的大块内存,由多个地址连续的页组成;

      object:由span按特定大小切分的小块内存,每一个可存储一个对象;

      按照用途,span面向内部管理,object面向对象分配。

    关于span

      内存分配器按照页数来区分不同大小的span,如以页数为单位将span存放到管理数组中,且以页数作为索引;

      span大小并非不变,在没有获取到合适大小的闲置span时,返回页数更多的span,然后进行剪裁,多余的页数构成新的span,放回管理数组;

      分配器还可以将相邻的空闲span合并,以构建更大的内存块,减少碎片提供更灵活的分配策略。

    分配的内存块大小

      在$GOROOT/src/runtime/malloc.go文件下可以找到相关信息。

    1 //malloc.go
    2 _PageShift = 13
    3 _PageSize  = 1<<  _PageShift  //8KB

      用于存储对象的object,按8字节倍数分为n种。如,大小为24的object可存储范围在17~24字节的对象。在造成一些内存浪费的同时减少了小块内存的规格,优化了分配和复用的管理策略。

      分配器还会将多个微小对象组合到一个object块内,以节约内存。

    1 //malloc.go
    2 _NumSizeClasses = 67
     1 //mheap.go
     2 type mspan struct {
     3     next *mspan   //双向链表 next span in list, or nil if none
     4     prev *mspan   //previous span in list, or nil if none
     5     list *mSpanList  //用于调试。TODO: Remove.
     6 
     7     //起始序号 = (address >> _PageShift)
     8     startAddr uintptr  //address of first byte of span aka s.base()
     9     npages    uintptr  //number of pages in span
    10 
    11     //待分配的object链表
    12     manualFreeList gclinkptr  //list of free objects in mSpanManual spans
    13 }

      分配器初始化时,会构建对照表存储大小和规格的对应关系,包括用来切分的span页数。

     1 //msize.go
     2 
     3  // Malloc small size classes.
     4  //
     5  // See malloc.go for overview.
     6  // See also mksizeclasses.go for how we decide what size classes to use.
     7 
     8  package runtime
     9 
    10  // 如果需要,返回mallocgc将分配的内存块的大小。
    11  func roundupsize(size uintptr) uintptr {
    12      if size < _MaxSmallSize {
    13          if size <= smallSizeMax-8 { 
    14              return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])
    15          } else {       
    16              return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]])
    17         }                                                                                                                                                                                                                                                                                
    18     }
    19      if size+_PageSize < size {                                                                                                                                                                                                                                                           
    20          return size
    21      }                                                                                                                                                                                                                                                                                    
    22      return round(size, _PageSize)
    23 }

       如果对象大小超出特定阈值限制,会被当做大对象(large object)特别对待。

    1 //malloc.go
    2 _MaxSmallSize = 32 << 10   //32KB

      这里的对象分类:

    • 小对象(tiny): size < 16byte;
    • 普通对象: 16byte ~ 32K;
    • 大对象(large):size > 32K;

    1.2 内存分配器

    分配器分为三个模块

      cache:每个运行期工作线程都会绑定一个cache,用于无锁object分配(Central组件其实也是一个缓存,但它缓存的不是小对象内存块,而是一组一组的内存page(一个page占4k大小))。

    1 //mcache.go
    2 type mcache struct{
    3     以spanClass为索引管理多个用于分配的span
    4     alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass  
    5 }

      central:为所有cache提供切分好的后备span资源。

    1 //mcentral.go
    2 type mcentral struct{
    3     spanclass   spanClass             //规格
    4   //链表:尚有空闲object的span
    5     nonempty mSpanList // list of spans with a free object, ie a nonempty free list      
    6     // 链表:没有空闲object,或已被cache取走的span
    7     empty    mSpanList // list of spans with no free objects (or cached in an mcache)
    8 }
    9     

      heap:管理闲置span,需要时间向操作系统申请新内存(堆分配器,以8192byte页进行管理)。

     1 type mheap struct{
     2     largealloc  uint64                  // bytes allocated for large objects 
     3     //页数大于127(>=127)的闲置span链表                                                                                                                     
     4     largefree   uint64                  // bytes freed for large objects (>maxsmallsize)    
     5     nlargefree  uint64                  // number of frees for large objects (>maxsmallsize) 
     6     //页数在127以内的闲置span链表数组                                                                                                                     
     7     nsmallfree  [_NumSizeClasses]uint64 // number of frees for small objects (<=maxsmallsize)
     8     //每个central对应一种sizeclass
     9     central [numSpanClasses]struct {
    10         mcentral mcentral
    11         pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
    12 }

      一个线程有一个cache对应,这个cache用来存放小对象。所有线程共享Central和Heap。 

    虚拟地址空间

      内存分配和垃圾回收都依赖连续地址,所以系统预留虚拟地址空间,用于内存分配,申请内存时,系统承诺但不立即分配物理内存。虚拟地址分成三个区域:

    • 页所属span指针数组                     spans 512MB              spans_mapped
    • GC标记位图                                bitmap 32GB              bit_map
    • 用户内存分配区域                        arena  512GB              arena_start  arena_used  arena_end

      三个数组组成一个高性能内存管理结构。使用arena地址向操作系统申请内存,其大小决定了可分配用户内存上限;bitmap为每个对象提供4bit 标记位,用以保存指针、GC标记等信息;创建span时,按页填充对应spans空间。这些区域的相关属性保存在heap里,其中包括递进的分配位置mapped/used。

    各个模块关系图如下:

    1.3 内存分配流程

    从对象的角度:

      1、计算待分配对象规格大小(size class);

      2、cache.alloc数组中找到对应规格的apan;

      3、span.freelist提取可用object,若该span.freelist为空从central获取新sapn;

      4、若central.nonempty为空,从heap.free/freelarge获取,并切分成object 链表;

      5、如heap没有大小合适的闲置span,向操作系统申请新内存块。

    释放流程:

      1、将标记为可回收的object交还给所属span.freelist;

      2、该span被放回central,可供任意cache重新获取使用;

      3、如span已回收全部object,则将其交还给heap,以便重新切分复用;

      4、定期扫描heap里长期闲置的span,释放其占用内存。

      (注:以上不包括大对象,它直接从heap分配和回收)

       cache为每个工作线程私有且不被共享,是实现高性能无锁分配内存的核心。central是在多个cache中提高object的利用率,避免浪费。回收操作将span交还给central后,该span可被其他cache重新获取使用。将span归还给heap是为了在不同规格object间平衡。

    2. 内存分配器初始化

      初始化流程:

      1 func mallocinit() {
      2     testdefersizes()
      3 
      4     if heapArenaBitmapBytes&(heapArenaBitmapBytes-1) != 0 {
      5         // heapBits需要位图上的模块化算法工作地址。
      6         throw("heapArenaBitmapBytes not a power of 2")
      7     }
      8 
      9     // //复制类大小以用于统计信息表。
     10     for i := range class_to_size {
     11         memstats.by_size[i].size = uint32(class_to_size[i])
     12     }
     13 
     14     // 检查 physPageSize.
     15     if physPageSize == 0 {
     16         // 操作系统初始化代码无法获取物理页面大小。
     17         throw("failed to get system page size")
     18     }
     19     if physPageSize < minPhysPageSize {
     20         print("system page size (", physPageSize, ") is smaller than minimum page size (", minPhysPageSize, ")
    ")
     21         throw("bad system page size")
     22     }
     23     if physPageSize&(physPageSize-1) != 0 {
     24         print("system page size (", physPageSize, ") must be a power of 2
    ")
     25         throw("bad system page size")
     26     }
     27 
     28     // 初始化堆。
     29     mheap_.init()
     30     //为当前对象绑定cache对象
     31     _g_ := getg()
     32     _g_.m.mcache = allocmcache()
     33 
     34     //创建初始 arena 增长提示。
     35     if sys.PtrSize == 8 && GOARCH != "wasm" {
     36         //在64位计算机上:
     37         // 1.从地址空间的中间开始,可以轻松扩展到连续范围,而无需运行其他映射。
     38         //
     39         // 2.这使Go堆地址调试时更容易识别。
     40         //
     41         // 3. gccgo中的堆栈扫描仍然很保守,因此将地址与其他数据区分开很重要。
     42         //
     43         //在AIX上,对于64位,mmaps从0x0A00000000000000开始设置保留地址,如果失败,则尝试0x1c00000000000000~0x7fc0000000000000。
     44         //  流程.
     45         for i := 0x7f; i >= 0; i-- {
     46             var p uintptr
     47             switch {
     48             case GOARCH == "arm64" && GOOS == "darwin":
     49                 p = uintptr(i)<<40 | uintptrMask&(0x0013<<28)
     50             case GOARCH == "arm64":
     51                 p = uintptr(i)<<40 | uintptrMask&(0x0040<<32)
     52             case GOOS == "aix":
     53                 if i == 0 {
     54                     //我们不会直接在0x0A00000000000000之后使用地址,以避免与非执行程序所完成的其他mmap发生冲突。
     55                     continue
     56                 }
     57                 p = uintptr(i)<<40 | uintptrMask&(0xa0<<52)
     58             case raceenabled:
     59                 // TSAN运行时要求堆的范围为[0x00c000000000,0x00e000000000)。
     60                 p = uintptr(i)<<32 | uintptrMask&(0x00c0<<32)
     61                 if p >= uintptrMask&0x00e000000000 {
     62                     continue
     63                 }
     64             default:
     65                 p = uintptr(i)<<40 | uintptrMask&(0x00c0<<32)
     66             }
     67             hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc())
     68             hint.addr = p
     69             hint.next, mheap_.arenaHints = mheap_.arenaHints, hint
     70         }
     71     } else {
     72         //在32位计算机上,需要更加关注保持可用堆是连续的。
     73         //
     74         // 1.我们为所有的heapArenas保留空间,这样它们就不会与heap交错。它们约为258MB。
     75         //
     76         // 2. 我们建议堆从二进制文件的末尾开始,因此我们有最大的机会保持其连续性。
     77         //
     78         // 3. 我们尝试放出一个相当大的初始堆保留。
     79 
     80         const arenaMetaSize = (1 << arenaBits) * unsafe.Sizeof(heapArena{})
     81         meta := uintptr(sysReserve(nil, arenaMetaSize))
     82         if meta != 0 {
     83             mheap_.heapArenaAlloc.init(meta, arenaMetaSize)
     84         }
     85 
     86         procBrk := sbrk0()
     87 
     88         p := firstmoduledata.end
     89         if p < procBrk {
     90             p = procBrk
     91         }
     92         if mheap_.heapArenaAlloc.next <= p && p < mheap_.heapArenaAlloc.end {
     93             p = mheap_.heapArenaAlloc.end
     94         }
     95         p = round(p+(256<<10), heapArenaBytes)
     96         // // 因为我们担心32位上的碎片,所以我们尝试进行较大的初始保留。
     97         arenaSizes := []uintptr{
     98             512 << 20,
     99             256 << 20,
    100             128 << 20,
    101         }
    102         for _, arenaSize := range arenaSizes {
    103             a, size := sysReserveAligned(unsafe.Pointer(p), arenaSize, heapArenaBytes)
    104             if a != nil {
    105                 mheap_.arena.init(uintptr(a), size)
    106                 p = uintptr(a) + size // For hint below
    107                 break
    108             }
    109         }
    110         hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc())
    111         hint.addr = p
    112         hint.next, mheap_.arenaHints = mheap_.arenaHints, hint
    113     }
    114 }

    大概流程:

      1、创建对象规格大小对照表;

      2、计算相关区域大小,并尝试从某个指定位置开始保留地址空间;

      3、在heap里保存区域信息,包括起始位置和大小;

      4、初始化heap其他属性。

      看一下保留地址操作细节:

     1 //mem_linux.go
     2 func sysReserve(v unsafe.Pointer, n uintptr) unsafe.Pointer {
     3     p, err := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0) //PORT_NONE: 页面无法访问;
     4     if err != 0 {
     5         return nil
     6     }
     7     return p
     8 }
     9 
    10 func sysMap(v unsafe.Pointer, n uintptr, sysStat *uint64) {
    11     mSysStatInc(sysStat, n)
    12 
    13     p, err := mmap(v, n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_FIXED|_MAP_PRIVATE, -1, 0)  //_MAP_FIXED: 必须使用指定起始位置
    14     if err == _ENOMEM {
    15         throw("runtime: out of memory")
    16     }
    17     if p != v || err != 0 {
    18         throw("runtime: cannot map pages in arena address space")
    19     }
    20 }

      函数mmap()要求操作系统内核创建新的虚拟存储器区域,可指定起始位置和长度。

    3. 内存分配

      编译器有责任尽可能使用寄存器和栈来存储对象,有助于提升性能,减少垃圾回收器的压力。

      以new函数为例看一下内存分配

     1 //test.go
     2 package main 
     3 
     4 import ()
     5 
     6 func test() *int {
     7     x :=new(int)
     8     *x = 0xAABB
     9     return x
    10 }
    11 
    12 func main(){
    13     println(*test())
    14 }

       在默认有内联优化的时候:                                                                                                                                                                                    

      内联优化是避免栈和抢占检查这些成本的经典优化方法。

      在没有内联优化的时候new函数会调用newobject在堆上分配内存。要在两个栈帧间传递对象,因此会在堆上分配而不是返回一个失效栈帧里的数据。而当内联后它实际上就成了main栈帧内的局部变量,无须去堆上操作。

      GO语言支持逃逸分析(eseape, analysis), 它会在编译期通过构建调用图来分析局部变量是否会被外部调用,从而决定是否可以直接分配在栈上。

      编译参数-gcflags "-m" 可输出编译优化信息,其中包括内联和逃逸分析。性能测试时使用go-test-benchemem参数可以输出堆分配次数统计。

    3.1 newobject分配内存的过程

     1 //mcache.go
     2 
     3 //小对象的线程(按Go,按P)缓存。 不需要锁定,因为它是每个线程的(每个P)。 mcache是从非GC的内存中分配的,因此任何堆指针都必须进行特殊处理。
     4 //go:not in heap
     5 type mcache struct {
     6     ...
     7     // Allocator cache for tiny objects w/o pointers.See "Tiny allocator" comm ent in malloc.go.
     8     // tiny指向当前微小块的开头,如果没有当前微小块,则为nil。
     9     //
    10     //  tiny是一个堆指针。 由于mcache位于非GC的内存中,因此我们通过在标记终止期间在releaseAll中将其清除来对其进行处理。
    11     tiny             uintptr
    12     tinyoffset       uintptr
    13     local_tinyallocs uintptr // 未计入其他统计信息的微小分配数
    14 
    15     // 其余的不是在每个malloc上访问的。
    16     alloc [numSpanClasses]*mspan // 要分配的范围,由spanClass索引
    17 }

      内置new函数的实现

      1 //malloc.go
      2 // implementation of new builtin
      3 // compiler (both frontend and SSA backend) knows the signature
      4 // of this function
      5 func newobject(typ *_type) unsafe.Pointer {
      6     return mallocgc(typ.size, typ, true)
      7 }
      8 
      9 // Allocate an object of size bytes.
     10 // Small objects are allocated from the per-P cache's free lists.
     11 // Large objects (> 32 kB) are allocated straight from the heap.
     12 ///分配一个大小为字节的对象。小对象是从per-P缓存的空闲列表中分配的。 大对象(> 32 kB)直接从堆中分配。
     13 func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
     14     if gcphase == _GCmarktermination { //垃圾回收有关
     15         throw("mallocgc called with gcphase == _GCmarktermination")
     16     }
     17 
     18     if size == 0 {
     19         return unsafe.Pointer(&zerobase)
     20     }
     21     if debug.sbrk != 0 {
     22         align := uintptr(16)
     23         if typ != nil {
     24             align = uintptr(typ.align)
     25         }
     26         return persistentalloc(size, align, &memstats.other_sys)  //围绕sysAlloc的包装程序,可以分配小块。没有相关的自由操作。用于功能/类型/调试相关的持久数据。如果align为0,则使用默认的align(当前为8)。返回的内存将被清零。考虑将持久分配的类型标记为go:notinheap。
     27     }
     28 
     29     // assistG是要为此分配收费的G,如果GC当前未激活,则为n。
     30     var assistG *g
     31 
     32      ...
     33 
     34     // Set mp.mallocing to keep from being preempted by GC.
     35     //加锁放防止GC被抢占。
     36     mp := acquirem()
     37     if mp.mallocing != 0 {
     38         throw("malloc deadlock")
     39     }
     40     if mp.gsignal == getg() {
     41         throw("malloc during signal")
     42     }
     43     mp.mallocing = 1
     44 
     45     shouldhelpgc := false
     46     dataSize := size
     47 
     48      //当前线程所绑定的cache
     49     c := gomcache()
     50     var x unsafe.Pointer
     51     // 判断分配的对象是否 是nil或非指针类型
     52     noscan := typ == nil || typ.kind&kindNoPointers != 0
     53     //微小对象
     54     if size <= maxSmallSize {
     55         //无须扫描非指针微小对象(16)
     56         if noscan && size < maxTinySize {
     57             // Tiny allocator.
     58             //微小的分配器将几个微小的分配请求组合到一个内存块中。当所有子对象均不可访问时,将释放结果存储块。子对象必须是noscan(没有指针),以确保限制可能浪费的内存量。
     59             //用于合并的存储块的大小(maxTinySize)是可调的。当前设置为16字节.
     60             //小分配器的主要目标是小字符串和独立的转义变量。在json基准上,分配器将分配数量减少了约12%,并将堆大小减少了约20%。
     61             off := c.tinyoffset
     62             // 对齐所需(保守)对齐的小指针。调整偏移量。
     63             if size&7 == 0 {
     64                 off = round(off, 8)
     65             } else if size&3 == 0 {
     66                 off = round(off, 4)
     67             } else if size&1 == 0 {
     68                 off = round(off, 2)
     69             }
     70             //如果剩余空间足够.  当前mcache上绑定的tiny 块内存空间足够,直接分配,并返回
     71             if off+size <= maxTinySize && c.tiny != 0 {
     72                 // 返回指针,调整偏移量为下次分配做好准备。
     73                 x = unsafe.Pointer(c.tiny + off)
     74                 c.tinyoffset = off + size
     75                 c.local_tinyallocs++
     76                 mp.mallocing = 0
     77                 releasem(mp)
     78                 return x
     79             }
     80             //当前mcache上的 tiny 块内存空间不足,分配新的maxTinySize块。就是从sizeclass=2的span.freelist获取一个16字节object。
     81             span := c.alloc[tinySpanClass]
     82             // 尝试从 allocCache 获取内存,获取不到返回0
     83             v := nextFreeFast(span)
     84             if v == 0 {
     85             // 没有从 allocCache 获取到内存,netxtFree函数 尝试从 mcentral获取一个新的对应规格的内存块(新span),替换原先内存空间不足的内存块,并分配内存,后面解析 nextFree 函数
     86                 v, _, shouldhelpgc = c.nextFree(tinySpanClass)
     87             }
     88             x = unsafe.Pointer(v)
     89             (*[2]uint64)(x)[0] = 0
     90             (*[2]uint64)(x)[1] = 0
     91             // 对比新旧两个tiny块剩余空间,看看我们是否需要用剩余的自由空间来替换现有的微型块。新块分配后其tinyyoffset = size,因此比对偏移量即可
     92             if size < c.tinyoffset || c.tiny == 0 {
     93                 //用新块替换
     94                 c.tiny = uintptr(x)
     95                 c.tinyoffset = size
     96             }
     97             //消费一个新的完整tiny块
     98             size = maxTinySize
     99         } else {
    100             // 这里开始 正常对象的 内存分配
    101             
    102             // 首先查表,以确定 sizeclass
    103             var sizeclass uint8
    104             if size <= smallSizeMax-8 {
    105                 sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]
    106             } else {
    107                 sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]
    108             }
    109             size = uintptr(class_to_size[sizeclass])
    110             spc := makeSpanClass(sizeclass, noscan)
    111             //找到对应规格的span.freelist,从中提取object
    112             span := c.alloc[spc]
    113             // 同小对象分配一样,尝试从 allocCache 获取内存,获取不到返回0
    114             v := nextFreeFast(span)
    115             
    116             //没有可用的object。从central获取新的span。
    117             if v == 0 {
    118                 v, span, shouldhelpgc = c.nextFree(spc)
    119             }
    120             x = unsafe.Pointer(v)
    121             if needzero && span.needzero != 0 {
    122                 memclrNoHeapPointers(unsafe.Pointer(v), size)
    123             }
    124         }
    125     } else {
    126         // 这里开始大对象的分配
    127 
    128         // 大对象的分配与 小对象 和普通对象 的分配有点不一样,大对象直接从 mheap 上分配
    129         var s *mspan
    130         shouldhelpgc = true
    131         systemstack(func() {
    132             s = largeAlloc(size, needzero, noscan)
    133         })
    134         s.freeindex = 1
    135         s.allocCount = 1
    136         //span.start实际由address >> pageshift生成。
    137         x = unsafe.Pointer(s.base())
    138         size = s.elemsize
    139     }
    140 
    141     // bitmap标记...
    142     // 检查出发条件,启动垃圾回收 ...
    143 
    144     return x
    145 }

    代码基本思路:

      1. 判定对象是大对象、小对象还是微小对象。

      2. 如果是微小对象:

        直接从 mcache 的alloc 找到对应 classsize 的 mspan;

        若当前mspan有足够空间,分配并修改mspan的相关属性(nextFreeFast函数中实现);

        若当前mspan的空间不足,则从 mcentral重新获取一块 对应 classsize的 mspan,替换原先的mspan,然后分配并修改mspan的相关属性;

      对于微小对象,它不能是指针,因为多个微小对象被组合到一个object里,显然无法应对辣鸡扫描。其次它从span.freelist获取一个16字节的object,然后利用偏移量来记录下一次分配的位置。

      3. 如果是小对象,内存分配逻辑大致同微小对象:

        首先查表,以确定 需要分配内存的对象的 sizeclass,并找到 对应 classsize的 mspan;

        若当前mspan有足够的空间,分配并修改mspan的相关属性(nextFreeFast函数中实现);

        若当前mspan没有足够的空间,从 mcentral重新获取一块对应 classsize的 mspan,替换原先的mspan,然后分配并修改mspan的相关属性;

      4. 如果是大对象,直接从mheap进行分配,这里的实现依靠 largeAlloc 函数实现,再看一下这个函数的实现。还是在malloc.go下面:

     1 func largeAlloc(size uintptr, needzero bool, noscan bool) *mspan {
     2     // print("largeAlloc size=", size, "
    ")
     3     
     4   // 内存溢出判断
     5     if size+_PageSize < size {
     6         throw("out of memory")
     7     }
     8   
     9   // 计算出对象所需的页数
    10     npages := size >> _PageShift
    11     if size&_PageMask != 0 {
    12         npages++
    13     }
    14 
    15     // Deduct credit for this span allocation and sweep if
    16     // necessary. mHeap_Alloc will also sweep npages, so this only
    17     // pays the debt down to npage pages.
    18     // 清理(Sweep)垃圾
    19     deductSweepCredit(npages*_PageSize, npages)
    20     
    21   // 分配函数的具体实现
    22     s := mheap_.alloc(npages, makeSpanClass(0, noscan), true, needzero)
    23     if s == nil {
    24         throw("out of memory")
    25     }
    26     s.limit = s.base() + size
    27   // bitmap 记录分配的span
    28     heapBitsForAddr(s.base()).initSpan(s)
    29     return s
    30 }

      再看看 mheap_.allo()函数的实现:

     1 //mheap.go
     2 // alloc allocates a new span of npage pages from the GC'd heap.
     3 // Either large must be true or spanclass must indicates the span's size class and scannability.
     4 // If needzero is true, the memory for the returned span will be zeroed.
     5 func (h *mheap) alloc(npage uintptr, spanclass spanClass, large bool, needzero bool) *mspan {
     6     // Don't do any operations that lock the heap on the G stack.
     7     // It might trigger stack growth, and the stack growth code needs
     8     // to be able to allocate heap.
     9     //如果needzero为true,则返回范围的内存将为零。
    10     //不要执行任何将堆锁定在G堆栈上的操作。
    11     //它可能会触发堆栈增长,而堆栈增长代码需要能够分配堆。
    12     var s *mspan
    13     systemstack(func() {
    14         s = h.alloc_m(npage, spanclass, large)
    15     })
    16 
    17     if s != nil {
    18         if needzero && s.needzero != 0 {
    19             memclrNoHeapPointers(unsafe.Pointer(s.base()), s.npages<<_PageShift)
    20         }
    21         s.needzero = 0
    22     }
    23     return s
    24 }
      mheap.alloc_m()根据页数从 heap 上面分配一个新的span,并且在 HeapMap 和 HeapMapCache 上记录对象的sizeclass。
     1 //mheap.go
     2 func (h *mheap) alloc_m(npage uintptr, spanclass spanClass, large bool) *mspan {
     3     _g_ := getg()
     4     if _g_ != _g_.m.g0 {
     5         throw("_mheap_alloc not on g0 stack")
     6     }
     7     lock(&h.lock)
     8 
     9     // 清理垃圾,内存块状态标记 省略...
    10     
    11     // 从 heap中获取指定页数的span
    12     s := h.allocSpanLocked(npage, &memstats.heap_inuse)
    13     if s != nil {
    14         // Record span info, because gc needs to be
    15         // able to map interior pointer to containing span.
    16         atomic.Store(&s.sweepgen, h.sweepgen)
    17         h.sweepSpans[h.sweepgen/2%2].push(s) // Add to swept in-use list.// 忽略
    18         s.state = _MSpanInUse
    19         s.allocCount = 0
    20         s.spanclass = spanclass
    21     // 重置span的状态
    22         if sizeclass := spanclass.sizeclass(); sizeclass == 0 {
    23             s.elemsize = s.npages << _PageShift
    24             s.divShift = 0
    25             s.divMul = 0
    26             s.divShift2 = 0
    27             s.baseMask = 0
    28         } else {
    29             s.elemsize = uintptr(class_to_size[sizeclass])
    30             m := &class_to_divmagic[sizeclass]
    31             s.divShift = m.shift
    32             s.divMul = m.mul
    33             s.divShift2 = m.shift2
    34             s.baseMask = m.baseMask
    35         }
    36 
    37         // update stats, sweep lists
    38         h.pagesInUse += uint64(npage)
    39         if large {
    40       // 更新 mheap中大对象的相关属性
    41             memstats.heap_objects++
    42             mheap_.largealloc += uint64(s.elemsize)
    43             mheap_.nlargealloc++
    44             atomic.Xadd64(&memstats.heap_live, int64(npage<<_PageShift))
    45             // Swept spans are at the end of lists.
    46       // 根据页数判断是busy还是 busylarge链表,并追加到末尾
    47             if s.npages < uintptr(len(h.busy)) {
    48                 h.busy[s.npages].insertBack(s)
    49             } else {
    50                 h.busylarge.insertBack(s)
    51             }
    52         }
    53     }
    54     // gc trace 标记,省略...
    55     unlock(&h.lock)
    56     return s
    57 }
      mheap.allocSpanLocked()函数分配一个给定大小的span,并将分配的span从freelist中移除。
      //mheap.go
    1
    func (h *mheap) allocSpanLocked(npage uintptr, stat *uint64) *mspan { 2 var list *mSpanList 3 var s *mspan 4 5 // Try in fixed-size lists up to max. 6 // 先尝试获取指定页数的span,如果没有,则试试页数更多的 7 for i := int(npage); i < len(h.free); i++ { 8 list = &h.free[i] 9 if !list.isEmpty() { 10 s = list.first 11 list.remove(s) 12 goto HaveSpan 13 } 14 } 15 // Best fit in list of large spans. 16 // 从 freelarge 上找到一个合适的span节点返回 ,下面继续分析这个函数 17 s = h.allocLarge(npage) // allocLarge removed s from h.freelarge for us 18 if s == nil { 19 // 如果 freelarge上找不到合适的span节点,就只有从 系统 重新分配了 20 // 后面继续分析这个函数 21 if !h.grow(npage) { 22 return nil 23 } 24 // 从系统分配后,再次到freelarge 上寻找合适的节点 25 s = h.allocLarge(npage) 26 if s == nil { 27 return nil 28 } 29 } 30 31 HaveSpan: 32 // 从 free 上面获取到了 合适页数的span 33 // Mark span in use. 省略.... 34 35 if s.npages > npage { 36 // Trim extra and put it back in the heap. 37 // 创建一个 s.napges - npage 大小的span,并放回 heap 38 t := (*mspan)(h.spanalloc.alloc()) 39 t.init(s.base()+npage<<_PageShift, s.npages-npage) 40 // 更新获取到的span s 的属性 41 s.npages = npage 42 h.setSpan(t.base()-1, s) 43 h.setSpan(t.base(), t) 44 h.setSpan(t.base()+t.npages*pageSize-1, t) 45 t.needzero = s.needzero 46 s.state = _MSpanManual // prevent coalescing with s 47 t.state = _MSpanManual 48 h.freeSpanLocked(t, false, false, s.unusedsince) 49 s.state = _MSpanFree 50 } 51 s.unusedsince = 0 52 // 将s放到spans 和 arenas 数组里面 53 h.setSpans(s.base(), npage, s) 54 55 *stat += uint64(npage << _PageShift) 56 memstats.heap_idle -= uint64(npage << _PageShift) 57 58 //println("spanalloc", hex(s.start<<_PageShift)) 59 if s.inList() { 60 throw("still in list") 61 } 62 return s 63 }
      mheap.allocLarge()函数从 mheap 的 freeLarge 树上面找到一个指定page数量的span,并将该span从树上移除,找不到则返回nil。
     1 //mheap.go
     2 func (h *mheap) allocLarge(npage uintptr) *mspan {
     3     // Search treap for smallest span with >= npage pages.
     4     return h.freelarge.remove(npage)
     5 }
     6 
     7 // 上面的 h.freelarge.remove 即调用这个函数
     8 // 典型的二叉树寻找算法
     9 func (root *mTreap) remove(npages uintptr) *mspan {
    10     t := root.treap
    11     for t != nil {
    12         if t.spanKey == nil {
    13             throw("treap node with nil spanKey found")
    14         }
    15         if t.npagesKey < npages {
    16             t = t.right
    17         } else if t.left != nil && t.left.npagesKey >= npages {
    18             t = t.left
    19         } else {
    20             result := t.spanKey
    21             root.removeNode(t)
    22             return result
    23         }
    24     }
    25     return nil
    26 }
      mheap.grow()函数在 mheap.allocSpanLocked 这个函数中,如果 freelarge上找不到合适的span节点,就只有从系统重新分配了,那我们接下来就继续分析一下这个函数的实现。
     1 func (h *mheap) grow(npage uintptr) bool {
     2     ask := npage << _PageShift
     3   // 向系统申请内存,后面继续追踪 sysAlloc 这个函数
     4     v, size := h.sysAlloc(ask)
     5     if v == nil {
     6         print("runtime: out of memory: cannot allocate ", ask, "-byte block (", memstats.heap_sys, " in use)
    ")
     7         return false
     8     }
     9 
    10     // Create a fake "in use" span and free it, so that the
    11     // right coalescing happens.
    12   // 创建 span 来管理刚刚申请的内存
    13     s := (*mspan)(h.spanalloc.alloc())
    14     s.init(uintptr(v), size/pageSize)
    15     h.setSpans(s.base(), s.npages, s)
    16     atomic.Store(&s.sweepgen, h.sweepgen)
    17     s.state = _MSpanInUse
    18     h.pagesInUse += uint64(s.npages)
    19   // 将刚刚申请的span放到 arenas 和 spans 数组里面
    20     h.freeSpanLocked(s, false, true, 0)
    21     return true
    22 }
      mheao.sysAlloc()
      1 func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {
      2    n = round(n, heapArenaBytes)
      3 
      4    // First, try the arena pre-reservation.
      5  // 从 arena 中 获取对应大小的内存, 获取不到返回nil
      6    v = h.arena.alloc(n, heapArenaBytes, &memstats.heap_sys)
      7    if v != nil {
      8    // 从arena获取到需要的内存,跳转到 mapped操作
      9        size = n
     10        goto mapped
     11    }
     12 
     13    // Try to grow the heap at a hint address.
     14  // 尝试 从 arenaHint向下扩展内存
     15    for h.arenaHints != nil {
     16        hint := h.arenaHints
     17        p := hint.addr
     18        if hint.down {
     19            p -= n
     20        }
     21        if p+n < p {
     22            // We can't use this, so don't ask.
     23      // 表名 hint.down = false 不能向下扩展内存
     24            v = nil
     25        } else if arenaIndex(p+n-1) >= 1<<arenaBits {
     26      // 超出 heap 可寻址的内存地址,不能使用
     27            // Outside addressable heap. Can't use.
     28            v = nil
     29        } else {
     30      // 当前hint可以向下扩展内存,利用mmap向系统申请内存
     31            v = sysReserve(unsafe.Pointer(p), n)
     32        }
     33        if p == uintptr(v) {
     34            // Success. Update the hint.
     35            if !hint.down {
     36                p += n
     37            }
     38            hint.addr = p
     39            size = n
     40            break
     41        }
     42        // Failed. Discard this hint and try the next.
     43        //
     44        // TODO: This would be cleaner if sysReserve could be
     45        // told to only return the requested address. In
     46        // particular, this is already how Windows behaves, so
     47        // it would simply things there.
     48        if v != nil {
     49            sysFree(v, n, nil)
     50        }
     51        h.arenaHints = hint.next
     52        h.arenaHintAlloc.free(unsafe.Pointer(hint))
     53    }
     54 
     55    if size == 0 {
     56        if raceenabled {
     57            // The race detector assumes the heap lives in
     58            // [0x00c000000000, 0x00e000000000), but we
     59            // just ran out of hints in this region. Give
     60            // a nice failure.
     61            throw("too many address space collisions for -race mode")
     62        }
     63 
     64        // All of the hints failed, so we'll take any
     65        // (sufficiently aligned) address the kernel will give
     66        // us.
     67        v, size = sysReserveAligned(nil, n, heapArenaBytes)
     68        if v == nil {
     69            return nil, 0
     70        }
     71 
     72        // Create new hints for extending this region.
     73        hint := (*arenaHint)(h.arenaHintAlloc.alloc())
     74        hint.addr, hint.down = uintptr(v), true
     75        hint.next, mheap_.arenaHints = mheap_.arenaHints, hint
     76        hint = (*arenaHint)(h.arenaHintAlloc.alloc())
     77        hint.addr = uintptr(v) + size
     78        hint.next, mheap_.arenaHints = mheap_.arenaHints, hint
     79    }
     80 
     81    // Check for bad pointers or pointers we can't use.
     82    {
     83        var bad string
     84        p := uintptr(v)
     85        if p+size < p {
     86            bad = "region exceeds uintptr range"
     87        } else if arenaIndex(p) >= 1<<arenaBits {
     88            bad = "base outside usable address space"
     89        } else if arenaIndex(p+size-1) >= 1<<arenaBits {
     90            bad = "end outside usable address space"
     91        }
     92        if bad != "" {
     93            // This should be impossible on most architectures,
     94            // but it would be really confusing to debug.
     95            print("runtime: memory allocated by OS [", hex(p), ", ", hex(p+size), ") not in usable address space: ", bad, "
    ")
     96            throw("memory reservation exceeds address space limit")
     97        }
     98    }
     99 
    100    if uintptr(v)&(heapArenaBytes-1) != 0 {
    101        throw("misrounded allocation in sysAlloc")
    102    }
    103 
    104    // Back the reservation.
    105    sysMap(v, size, &memstats.heap_sys)
    106 
    107 mapped:
    108    // Create arena metadata.
    109  // 根据 v 的address,计算出 arenas 的L1 L2
    110    for ri := arenaIndex(uintptr(v)); ri <= arenaIndex(uintptr(v)+size-1); ri++ {
    111        l2 := h.arenas[ri.l1()]
    112        if l2 == nil {
    113      // 如果 L2 为 nil,则分配 arenas[L1]
    114            // Allocate an L2 arena map.
    115            l2 = (*[1 << arenaL2Bits]*heapArena)(persistentalloc(unsafe.Sizeof(*l2), sys.PtrSize, nil))
    116            if l2 == nil {
    117                throw("out of memory allocating heap arena map")
    118            }
    119            atomic.StorepNoWB(unsafe.Pointer(&h.arenas[ri.l1()]), unsafe.Pointer(l2))
    120        }
    121        
    122    // 如果 arenas[ri.L1()][ri.L2()] 不为空 说明已经实例化过了
    123        if l2[ri.l2()] != nil {
    124            throw("arena already initialized")
    125        }
    126        var r *heapArena
    127    // 从 arena 上分配内存
    128        r = (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys))
    129        if r == nil {
    130            r = (*heapArena)(persistentalloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys))
    131            if r == nil {
    132                throw("out of memory allocating heap arena metadata")
    133            }
    134        }
    135 
    136        // Store atomically just in case an object from the
    137        // new heap arena becomes visible before the heap lock
    138        // is released (which shouldn't happen, but there's
    139        // little downside to this).
    140        atomic.StorepNoWB(unsafe.Pointer(&l2[ri.l2()]), unsafe.Pointer(r))
    141    }
    142    // ...
    143    return
    144 }

      大对象的分配流程至此结束。

    3.2 小对象和微小对象的分配

      nextFreeFast()函数返回 span 上可用的地址,如果找不到 则返回0

     1 func nextFreeFast(s *mspan) gclinkptr {
     2   // 计算s.allocCache从低位起有多少个0
     3     theBit := sys.Ctz64(s.allocCache) // Is there a free object in the allocCache?
     4     if theBit < 64 {
     5     
     6         result := s.freeindex + uintptr(theBit)
     7         if result < s.nelems {
     8             freeidx := result + 1
     9             if freeidx%64 == 0 && freeidx != s.nelems {
    10                 return 0
    11             }
    12       // 更新bitmap、可用的 slot索引
    13             s.allocCache >>= uint(theBit + 1)
    14             s.freeindex = freeidx
    15             s.allocCount++
    16       // 返回 找到的内存的地址
    17             return gclinkptr(result*s.elemsize + s.base())
    18         }
    19     }
    20     return 0
    21 }

      mcache.nextFree()函数。如果 nextFreeFast 找不到 合适的内存,就会进入这个函数。nextFree 如果在cached span 里面找到未使用的object,则返回,否则,调用refill 函数,从 central 中获取对应classsize的span,然后 从新的span里面找到未使用的object返回。

     1 //mcache.go
     2 func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
     3     // 先找到 mcache 中 对应 规格的 span
     4   s = c.alloc[spc]
     5     shouldhelpgc = false
     6   // 在 当前span中找到合适的 index索引
     7     freeIndex := s.nextFreeIndex()
     8     if freeIndex == s.nelems {
     9         // The span is full.
    10     // freeIndex == nelems 时,表示当前span已满
    11         if uintptr(s.allocCount) != s.nelems {
    12             println("runtime: s.allocCount=", s.allocCount, "s.nelems=", s.nelems)
    13             throw("s.allocCount != s.nelems && freeIndex == s.nelems")
    14         }
    15     // 调用refill函数,从 mcentral 中获取可用的span,并替换掉当前 mcache里面的span
    16         systemstack(func() {
    17             c.refill(spc)
    18         })
    19         shouldhelpgc = true
    20         s = c.alloc[spc]
    21         
    22     // 再次到新的span里面查找合适的index
    23         freeIndex = s.nextFreeIndex()
    24     }
    25 
    26     if freeIndex >= s.nelems {
    27         throw("freeIndex is not valid")
    28     }
    29     
    30   // 计算出来 内存地址,并更新span的属性
    31     v = gclinkptr(freeIndex*s.elemsize + s.base())
    32     s.allocCount++
    33     if uintptr(s.allocCount) > s.nelems {
    34         println("s.allocCount=", s.allocCount, "s.nelems=", s.nelems)
    35         throw("s.allocCount > s.nelems")
    36     }
    37     return
    38 }

    mcache.refill()函数

      Refill 根据指定的sizeclass获取对应的span,并作为 mcache的新的sizeclass对应的span

     1 //mcache.go
     2 func (c *mcache) refill(spc spanClass) {
     3     _g_ := getg()
     4 
     5     _g_.m.locks++
     6     // Return the current cached span to the central lists.
     7     s := c.alloc[spc]
     8 
     9     if uintptr(s.allocCount) != s.nelems {
    10         throw("refill of span with free space remaining")
    11     }
    12     
    13   // 判断s是不是 空的span
    14     if s != &emptymspan {
    15         s.incache = false
    16     }
    17     // 尝试从 mcentral 获取一个新的span来代替老的span
    18     // Get a new cached span from the central lists.
    19     s = mheap_.central[spc].mcentral.cacheSpan()
    20     if s == nil {
    21         throw("out of memory")
    22     }
    23 
    24     if uintptr(s.allocCount) == s.nelems {
    25         throw("span has no free space")
    26     }
    27     // 更新mcache的span
    28     c.alloc[spc] = s
    29     _g_.m.locks--
    30 }

    如果 从 mcentral 找不到对应的span,就会开始内存扩张,和上面分析的 mheap.alloc就相同了

    4. 总结

    1. 判定对象大小:

    2. 若是微小对象:

    1. 从 mcache 的 alloc 找到对应 classsize 的 mspan;
    2. 当前mspan有足够的空间时,分配并修改mspan的相关属性(nextFreeFast函数中实现);
    3. 若当前mspan没有足够的空间,从 mcentral 重新获取一块对应 classsize的 mspan,替换原先的mspan,然后分配并修改mspan的相关属性;
    4. 若 mcentral 没有足够的对应的classsize的span,则去向mheap申请;
    5. 若对应classsize的span没有了,则找一个相近的classsize的span,切割并分配;
    6. 若找不到相近的classsize的span,则去向系统申请,并补充到mheap中;

    3. 若是小对象,内存分配逻辑大致同小对象:

    1. 查表以确定需要分配内存的对象的 sizeclass,找到 对应classsize的 mspan;
    2. mspan有足够的空间时,分配并修改mspan的相关属性(nextFreeFast函数中实现);
    3. 若当前mspan没有足够的空间,从 mcentral重新获取一块对应 classsize的 mspan,替换原先的mspan,然后分配并修改mspan的相关属性;
    4. 若mcentral没有足够的对应的classsize的span,则去向mheap申请;
    5. 若对应classsize的span没有了,则找一个相近的classsize的span,切割并分配
    6. 若找不到相近的classsize的span,则去向系统申请,并补充到mheap中

    4. 若是大对象,直接从mheap进行分配

    1. 若对应classsize的span没有了,则找一个相近的classsize的span,切割并分配;
    2. 若找不到相近的classsize的span,则去向系统申请,并补充到mheap中;
  • 相关阅读:
    《机电传动控制》学习笔记08-1
    《机电传动控制》学习笔记-07
    《机电传动控制》学习笔记-06
    《机电传动控制》学习笔记05-2
    《机电传动控制》学习笔记05-1
    《团队项目》日志一
    《实时控制软件》第四周作业
    《实时控制软件》第三周作业
    《实时控制软件》第二周作业
    《机电传动控制》PLC仿真
  • 原文地址:https://www.cnblogs.com/33debug/p/12068699.html
Copyright © 2011-2022 走看看