1 span
golang将内存分成了从小到大的67个级别的span
span的划分方式是依据span中的元素大小,而不是一块span的大小。
所以当具体对象需要分配内存时,并不是直接分配span,而是分配不同级别的span中的元素
具体数据在golang的runtime
包下的sizeclasses.go
文件中(数据太长不在此展示)
但是源码非常有必要一看,尤其是里面某些字段,重点看一下allocCache
、freeindex
、nelems
后面的源码重点的地方会涉及到这三个,如果不理解就可能看不不懂后面的源码(一些GC相关的字段已删去,太长了…)
type mspan struct {
_ sys.NotInHeap
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
// freeindex is the slot index between 0 and nelems at which to begin scanning
// for the next free object in this span.
// Each allocation scans allocBits starting at freeindex until it encounters a 0
// indicating a free object. freeindex is then adjusted so that subsequent scans begin
// just past the newly discovered free object.
//
// If freeindex == nelem, this span has no free objects.
//
// allocBits is a bitmap of objects in this span.
// If n >= freeindex and allocBits[n/8] & (1<<(n%8)) is 0
// then object n is free;
// otherwise, object n is allocated. Bits starting at nelem are
// undefined and should never be referenced.
//
// Object n starts at address n*elemsize + (start << pageShift).
freeindex uintptr
// TODO: Look up nelems from sizeclass and remove this field if it
// helps performance.
nelems uintptr // number of object in the span.
// Cache of the allocBits at freeindex. allocCache is shifted
// such that the lowest bit corresponds to the bit freeindex.
// allocCache holds the complement of allocBits, thus allowing
// ctz (count trailing zero) to use it directly.
// allocCache may contain bits beyond s.nelems; the caller must ignore
// these.
allocCache uint64
}
2 三级对象管理
go采用了TCmalloc的思想,采取了三级结构mcache
、mcentral
、mheap
2.1 mcache
-
每个逻辑处理器P都存储了一个本地span缓存,叫做mcache
-
协程需要内存可以直接冲mcache获取,因为同一时间只有一个协程运行在P上,所以这个过程不需要加锁。
-
mcache包中含有所有大小规格的mspan,但每一种都有一个,除了上面说到的0级span以外,mcache的span都来自mcentral。
2.2 mcentral
-
mcentral是被所有逻辑处理器P所共享的
-
mcentral对象收集所有给定规格大小的span
-
每个mcentral都包含两个mspan链表:一个表示有空闲对象的mspan,一个表示没有空闲对象的span
-
除了0级mspan,每个级别的span都会有一个mcentral用于管理span链表,所有的mcentral都是一个数组,由mheap进行管理
2.3 mheap
-
mheap的作用不只是管理mcentral,大对象页通过mheap进行分配。
-
对mheap的操作必须全局加锁,由此mcache、mcentral可以看作某种形式的缓存
3 四级内存块管理
根据对象的大小,golang将内存返程了如图所示的HeapArea、chunk、与page四种内存块进行管理
-
HeapArea内存块最大,在unix64占64MB
-
chunk 占512KB
-
span根据级别的大小的不同而不同,但必须是page的倍数
-
一个page占8KB
不同的内存块用于不同的场景,便于高效的对内存进行管理
4 对象分配
内存分配时,将对象划分为微小对象
、小对象
、大对象
内存分配的主要逻辑在runtime/malloc.go
的878行
的mallocgc
函数
函数的主要逻辑如下:
// Allocate an object of size bytes.
// Small objects are allocated from the per-P cache's free lists.
// Large objects (> 32 kB) are allocated straight from the heap.
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
if size <= maxSmallSize {
if noscan && size < maxTinySize {
// Tiny allocator.
// 微小对象分配
} else {
// 小对象
}
} else {
// 大对象
}
}
4.1 微小对象
go将小于16字节的对象划分为微小对象。划分微小对象的主要目的是处理极小的字符串和独立的转义变量。
微小对象会被放到2级span中(2级span的大小为16字节)
- 小对象分配源码
// 1. 首先,微小对象会按照2、4、8字节对齐
off := c.tinyoffset
if size&7 == 0 {
off = alignUp(off, 8)
} else if goarch.PtrSize == 4 && size == 12 {
off = alignUp(off, 8)
} else if size&3 == 0 {
off = alignUp(off, 4)
} else if size&1 == 0 {
off = alignUp(off, 2)
}
// 查看当前分配的元素中是否有剩余的空间
if off+size <= maxTinySize && c.tiny != 0 {
x = unsafe.Pointer(c.tiny + off)
// 分配完成后offset的位置也需要增加,为下次分配做准备
c.tinyoffset = off + size
c.tinyAllocs++
mp.mallocing = 0
releasem(mp)
return x
}
// Allocate a new maxTinySize block.
span = c.alloc[tinySpanClass]
// 如果当前span的空间不够,将尝试从mcache中查找span中的快速下一个可用的元素
v := nextFreeFast(span)
if v == 0 {
// 如果在mcache没有,则尝试去mcentral中查找
v, span, shouldhelpgc = c.nextFree(tinySpanClass)
}
x = unsafe.Pointer(v)
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
// See if we need to replace the existing tiny block with the new one
// based on amount of remaining free space.
if !raceenabled && (size < c.tinyoffset || c.tiny == 0) {
// Note: disabled when race detector is on, see comment near end of this function.
c.tiny = uintptr(x)
c.tinyoffset = size
}
size = maxTinySize
4.1.1 mcache缓存位图
查找空闲元素空间时,首先要从mcache中找到对应的mspan,mspan中有allocCache字段其作为一个位
图,用于标记span元素是否被分配
- nextFreeFast函数源码,重要内容我都写道注释里,方便与代码结合
// nextFreeFast returns the next free object if one is quickly available.
// Otherwise it returns 0.
func nextFreeFast(s *mspan) gclinkptr {
// allocCache 是小端序,右边最后一位代表span第一个元素
// TrailingZeros64函数代表s.allocCache从右边开始第一个0的位置
theBit := sys.TrailingZeros64(s.allocCache) // Is there a free object in the allocCache?
// 还要注意一个十分重要的问题:
// 在runtime/mheap.go的1370行的initSpan函数中有关于allocCache 字段的初始化问题
// 在1398~1400行有关于freeindex和allocCache初始化的代码,拷贝如下
// s.freeindex = 0
// s.allocCache = ^uint64(0) // all 1s indicating all free.
// 可以看到allocCache字段初始化后,所有的比特位都是1
// 再结合下面几行代码:s.allocCache >>= uint(theBit + 1)可以看出:
// 如果theBit < 64代表则表示allocCache所有位置都是0,
if theBit < 64 {
// 当theBit的末尾一直是`...111111`的时候,theBit总是0
// 确保小于freeindex的都已经被分配
result := s.freeindex + uintptr(theBit)
if result < s.nelems {
// 找到可分配的位置
freeidx := result + 1
// 如果这个位置是64的倍数且freeidx != s.nelems
if freeidx%64 == 0 && freeidx != s.nelems {
return 0
}
s.allocCache >>= uint(theBit + 1)
s.freeindex = freeidx
s.allocCount++
return gclinkptr(result*s.elemsize + s.base())
}
}
return 0
}
4.1.2 mcentral遍历span
当前的span中没有可用的元素,这是就需要从mcentral中加锁寻找。
查找时会遍历mcentral中的有空闲元素的链表和没有空闲元素的链表,去查找有没有合适的span
之所以还会去遍历没有空闲元素的链表,是因为有些span虽然被标记为空闲,但是还没来得及清理,这
些span在清扫后仍然可以使用。
如果在mcentral中查找有空闲的span,将其赋值到,并更新allocCache,同时需要将span添加mcentral
的empty链表中去。
4.1.3 mheap缓存查找
go1.14之后每个逻辑处理器都维护了一个page cache
type pageCache struct {
base uintptr
cache uint64
scav uint64
}
mheap会首先查找,每个逻辑处理器P中的page cache字段,cache字段页代表一个位图
每一位都代表一个page(8KB),由于cache为uint64,因此一共可以提供512KB的连续虚拟内存。
当需要分配的内存小于512/4=128KB时需要首先从cache中分配。
当分配的page过大或者在逻辑处理器P的cache没有找到可用的page就需要对mheap加锁在一颗基数树中查找
4.1.4 操作系统内存申请
当基数树中不到相应的内存时,需要从操作系统获取内存,在UNIX系统中最终使用mmap系统调用操作系统申请内存,
每次像操作系统申请内存的大小必须为heapArea的倍数。
heapArea是和平台有关的内存大小。其大小为64MB。
注意:这里申请的内存为虚拟内存,只有实际写入的空间为程序实际占用的大小
4.2 小对象
当对象不属于小对象时,在内存分配时会继续判断是否为小对象,小对象指小于32KB的对象。
分配空间时会计算小对象对应哪一个等级的span,并在指定的span中查找
此后的操作和微小对象分配一样,小对象的分配经理mcache、mcentral、mheap位图、mheap基数树、操作系统分配的过程
4.3 大对象
大对象指大于32KB的对象,内存分配时直接通过mheap进行分配。大对象的分配经历:mheap基数树查找、操作系统分配的过程,每个大对象都时一个特殊的span,级别为0