基本策略

  1. 每次从操作系统申请一个大块内存(比如1MB),以此减少系统调用
  2. 将申请到的大块内存按照特定大小预先切分成小块,构成链表
  3. 为对象分配内存时,只需从大小合适的链表提取一个小块即可
  4. 回收对象内存时,将该小块内存重新归还到原链表,以便复用
  5. 如果闲置内存过多,则尝试归还部分内存给操作系统,降低整体开销

tips:内存分配只管理内存块,并不关心对象状态。且它不会主动回收内存,垃圾回收器在完成清理操作后,触发内存分配器的回收操作。

内存块

管理的内存块分为两种

  • span:由多个地址连续的页(page)组成的大块内存(面向内部管理)
  • object: 将span按特定大小切分成多个小块,每个小块可以存储一个对象(面向对象分配)
  1. 页数区分不同大小的span,需要时按页数作为索引查找。没有合适的就取更大的进行裁剪,多余部分放回管理数组。并会将地址相邻的span合并,减少碎片
_PageShift      = 13
...
pageShift = _PageShift
_PageSize = 1 << _PageShift // 8kb
  1. 对于存储对象的object,按8字节倍数分为n种。如24字节的可以存储17-24字节的对象。初始化时,会构建存储大小和规格的对应关系,切分
_NumSizeClasses = 68
...
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16...}
var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1...}
var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3...}
var size_to_class128 = [(_MaxSmallSize-smallSizeMax)/largeSizeDiv + 1]uint8{32, 33, 34, 35...}
...
// 小对象尺寸上限
_MaxSmallSize   = 32768

结构体

type mspan struct {
	next *mspan     // next span in list, or nil if none
	prev *mspan     // previous span in list, or nil if none
	list *mSpanList // For debugging. TODO: Remove.

	startAddr uintptr // address of first byte of span aka s.base()
	npages    uintptr // number of pages in span

	manualFreeList gclinkptr // list of free objects in mSpanManual spans

	allocBits  *gcBits
	gcmarkBits *gcBits

	// sweep generation:
	// if sweepgen == h->sweepgen - 2, the span needs sweeping
	// if sweepgen == h->sweepgen - 1, the span is currently being swept
	// if sweepgen == h->sweepgen, the span is swept and ready to use
	// if sweepgen == h->sweepgen + 1, the span was cached before sweep began and is still cached, and needs sweeping
	// if sweepgen == h->sweepgen + 3, the span was swept and then cached and is still cached
	// h->sweepgen is incremented by 2 after every GC
	sweepgen    uint32
	...
}

管理组件

malloc.go:base on tcmalloc,在性能与内存利用率之间做出平衡

  • cache:每个运行期工作线程都会绑定一个cache,用于无锁object分配
  • central:为所有cache提供切分好的后备span资源
  • heap:管理限制span,需要时向操作系统申请新内存

结构体

type mheap struct {
	allspans []*mspan // all spans out there
	// 每个central对应一种sizeclass
	central [numSpanClasses]struct {
		mcentral mcentral
		pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
	}
}

type mcentral struct {
	spanclass spanClass // 规格
	// 由于每次gc循环sweepgen增加2,所以清理过的spans为partial[sweepgen/2%2]
	// 以下都[2],其中一个上清理过的,一个是没清理过的
	partial [2]spanSet // list of spans with a free object
	full    [2]spanSet // list of spans with no free objects
}

type mcache struct {
	...
	alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
}

分配流程

  1. 计算待分配对象的规格(size class)
  2. 从cache.alloc数组中找到对应的规格相同的span
  3. 从span.manualFreeList提取可用的object
  4. 如果manualFreeList为空,则从central获取新span
  5. 如果central没有span则从heap中拿

central分配span

  1. 先看已扫描的partial列表中是否有span,有则pop
  2. 100次循环,看未扫描的partial列表,如果sweepgen=sg-2,说明需要被扫描。此时将它进行扫描设置为sg-1,然后用它
  3. 100次循环,未扫描full列表,同上。通过span的allocCache找到下一个freeIndex,返回。如果满了就把它丢进需要清扫的队列,继续循环。
  4. 实在没有了,就只能去heap里申请一个。