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间平衡。
来源:CSDN
作者:e5354966xieo1
链接:https://blog.csdn.net/e5354966xieo1/article/details/103730626