golang内存分配与管理

眉间皱痕 提交于 2019-12-27 16:33:39

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间平衡。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!