概述
Go是内置运行时的编程语言,像这种语言通常会抛弃传统的内存分配方式,改由自主管理
这样可以完成类似预分配、内存池等操作,以避开系统调用带来的性能问题,每次分配内存都需要系统调用
也是为了更好地垃圾回收
基本策略
1、每次从操作系统申请一大块内存,以减少系统调用
2、将申请的大块内存按照特定的大小预先分割成小块,构成链表
3、在为对象分配内存时,只需要按照大小找到合适的链表提取一个小块即可
4、回收对象内存时,将小块重新放回链表,以复用
5、如果闲置内存过多,则归还部分内存给操作系统,减小整体开销
内存块
内存分配器将其管理的内存块分配成两种
1、span:由多个地址连续的 page组成的大块内存
2、object:将 span按特定大小分割成的大小块,每个小块存储一个对象
span 的大小不是固定的,在获取闲置的 span时,如果没有找到合适的就会找更大的,然后裁剪多余的部分构成新的 span
不仅如此,分配器还会尝试将相邻的空闲 span合并,构建更大的内存块,减少碎片
分配器数据结构
fixalloc:固定大小的堆外对象的自由列表分配器,用于管理分配器使用的存储
mheap: Go程序所持有的 malloc堆,以页(8192字节)粒度管理
mspan: mheap管理的正在使用的页面的运行,内存管理的基础单元,直接存储数据的地方
mcentral:收集给定大小类的所有span,备用
mcache:每个运行期的goroutine都会绑定的一个mcache,GMP并发模型的 p的mspan缓存,分配goroutine运行中所需要的 span
mstats:分配统计信息。
mheap.go
type mspan struct {
next *mspan
prev *mspan // 双向链表
list *mSpanList
startAddr uintptr // span的第一个字节地址
npages uintptr // span中的页数
manualFreeList gclinkptr // span中空闲对象的列表
。。。。
}
mcentral.go
type mcentral struct {
spanclass spanClass
partial [2]spanSet // 具有空闲对象的span列表
full [2]spanSet // 没有空闲对象的span列表
}
mcache.go
type mcache struct {
nextSample uintptr
scanAlloc uintptr
// 有指针的小对象的分配器缓存, tiny指向当前小块的开始,tiny是堆指针。因为mcache在非gc内存中
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
alloc [numSpanClasses]*mspan // 要分配的span,由spanClass索引
stackcache [_NumStackOrders]stackfreelist
flushGen uint32
}
msize.go
// mallocgc分配内存块的大小
func roundupsize(size uintptr) uintptr {
if size < _MaxSmallSize {
if size <= smallSizeMax-8 {
return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
} else {
return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])
}
}
if size+_PageSize < size {
return size
}
return alignUp(size, _PageSize)
}
sizeclasses.go
const (
_MaxSmallSize = 32768
smallSizeDiv = 8
smallSizeMax = 1024
largeSizeDiv = 128
_NumSizeClasses = 68
_PageShift = 13
)
如果对象超过特定的阈值限制,会被当做大对象特别对待
tcmalloc框架
Golang 基本复用了 tcmalloc框架,分别对应前端(cache)、中端(central)、后端(heap)
前端是一个高速缓存,提供快速的内存分配和内存释放
中端负责填充前端缓存
后端负责管理和向操作系统申请内存
管理组件
分配器由三种组成
1、cache:每个运行期的工作线程都会绑定一个cache,用于无锁 object分配
2、central:为所有 cache提供分割好的备用 span
3、heap:管理闲置的 span,需要时向操作系统申请新内存
分配过程
1、计算待分配对象对应的块大小 spanclass
2、从 cache中 alloc数组中找出合适的 span
3、从空闲链表中提取可用的 object
4、如果链表为空,则从 central 获取新的 span
5、如果 central 中partial空闲链表为空,则从 heap获取,并分割成新的 object链表
6、如果 heap也没有合适的闲置 span,则向操作系统申请新的内存
释放过程
1、将标记为可回收的 object放回链表
2、该 span放回 central,任意的 cache可复用
3、如果 span全部收回 object,则返还 heap,以便重新分割复用
4、定期将 heap中长时间闲置的 span释放内存
不包括大对象,大对象直接从 heap中分配和回收
内存结构
简单说就是用三个数组组成一个高性能内存管理结构
这三个数组可按需同步线性扩张,不需要预先分配内存
1、arena也就是 heap,使用 arena地址向操作系统申请内存,其大小决定了可分配用户内存的上限
2、位图 bitmap用以保存指针、GC标记等信息,arena对应的某个地址是否存在对象
3、创建 span时按页填充对应的 spans空间
回收 object时,只需将其地址按页对齐就可以找到所属的 span
相邻 span也可以做合并操作
Go将内存分割为特定大小的67钟大内存块,再分割成连续的小块,小块就是page,67种 span
sizeclass.go
// class bytes/obj bytes/span objects tail waste max waste min align
// 1 8 8192 1024 0 87.50% 8
// 2 16 8192 512 0 43.75% 16
// 3 24 8192 341 8 29.24% 8
// 4 32 8192 256 0 21.88% 32
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
// 64 24576 24576 1 0 11.45% 8192
// 65 27264 81920 3 128 10.00% 128
// 66 28672 57344 2 0 4.91% 4096
// 67 32768 32768 1 0 12.50% 8192
注意几点
1、初始化时预留了 544 GB的虚拟地址空间,但是并没有分配内存
2、申请内存时,仅承诺但不立即分配物理内存
3、物理内存分配在写操作导致缺页异常调度时发生,按页提供
内存分配
malloc.go
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.size, typ, true)
}
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// 当前线程所绑定的 cache
c := getMCache(mp)
if size <= maxSmallSize {
// 无需扫描非指针微小对象(小于16B)
if noscan && size < maxTinySize {
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)
c.tinyoffset = off + size
c.tinyAllocs++
mp.mallocing = 0
releasem(mp)
return x
}
// 获取新的 tiny块
span = c.alloc[tinySpanClass]
v := nextFreeFast(span)
if v == 0 {
v, span, shouldhelpgc = c.nextFree(tinySpanClass)
}
x = unsafe.Pointer(v)
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
// 基于剩余空闲空间的数量,查看是否需要用新块替换现有的小块
if !raceenabled && (size < c.tinyoffset || c.tiny == 0) {
c.tiny = uintptr(x)
c.tinyoffset = size
}
size = maxTinySize
} else {
// 普通小对象(大于16B,小于32KB)
var sizeclass uint8
// 查表,以确定 sizeclass
if size <= smallSizeMax-8 {
sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]
} else {
sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]
}
size = uintptr(class_to_size[sizeclass])
// 从对应大小合适的 span空闲链表中提取 object
spc := makeSpanClass(sizeclass, noscan)
span = c.alloc[spc]
v := nextFreeFast(span)
// 如果没有可用的 object,就从 central获取新的 span,重新提取 object
if v == 0 {
v, span, shouldhelpgc = c.nextFree(spc)
}
// 归零
x = unsafe.Pointer(v)
if needzero && span.needzero != 0 {
memclrNoHeapPointers(unsafe.Pointer(v), size)
}
}
} else {
// 大对象直接从 heap中分配 span,(大于32KB)
shouldhelpgc = true
span = c.allocLarge(size, noscan)
span.freeindex = 1
span.allocCount = 1
size = span.elemsize
// 归零
x = unsafe.Pointer(span.base())
if needzero && span.needzero != 0 {
if noscan {
delayedZeroing = true
} else {
memclrNoHeapPointers(x, size)
}
}
}
// bitmap 标记
// 扫描,垃圾回收
。。。。。。。。。。。。。。。。。。。。。。。。
return x
}
mcentral.go
func (c *mcentral) cacheSpan() *mspan {
// 清理 sweep 垃圾
。。。。。。。。。。。。。。。。。。。。。。。
sg := mheap_.sweepgen
if s = c.partialSwept(sg).pop(); s != nil {
goto havespan
}
sl = sweep.active.begin()
if sl.valid {
// 遍历 partial 具有空闲对象的span列表
for ; spanBudget >= 0; spanBudget-- {
s = c.partialUnswept(sg).pop()
if s == nil {
break
}
if s, ok := sl.tryAcquire(s); ok {
// 找到了可用 span
s.sweep(true)
sweep.active.end(sl)
goto havespan
}
}
// 遍历 full 没有空闲对象的span列表
for ; spanBudget >= 0; spanBudget-- {
s = c.fullUnswept(sg).pop()
if s == nil {
break
}
if s, ok := sl.tryAcquire(s); ok {
s.sweep(true)
freeIndex := s.nextFreeIndex()
// 找到具有空闲的 span
if freeIndex != s.nelems {
s.freeindex = freeIndex
sweep.active.end(sl)
goto havespan
}
c.fullSwept(sg).push(s.mspan)
}
}
sweep.active.end(sl)
}
if trace.enabled {
traceGCSweepDone()
traceDone = true
}
// 两个链表都没有可用的 span,所以扩张
// 无法从 mcentral获取一个span,从 mheap获取一个span
s = c.grow()
if s == nil {
return nil
}
// 一个应该有空闲槽的span
havespan:
if trace.enabled && !traceDone {
traceGCSweepDone()
}
n := int(s.nelems) - int(s.allocCount)
if n == 0 || s.freeindex == s.nelems || uintptr(s.allocCount) == s.nelems {
throw("span has no free objects")
}
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
// 初始化 cache
s.refillAllocCache(whichByte)
s.allocCache >>= s.freeindex % 64
return s
}
看出从 central获取 span时,优先提取已有的资源,哪怕是要先执行清理操作。
只有现有资源也无法满足时,才会去 heap获取 span,重新分割成 object链表
回收
回收并非释放,整个内存分配器的核心就是内存复用,不再使用的内存会被放回合适的位置,等待下一次的分配
只有当空闲的内存过多时,才会考虑释放
回收以 span为单位,通过比对 bitmap中的扫描标记,将 object归还 span,再上交 central或 heap复用
Golang发展史上,有几个版本的改动比较重要
go 1.3 之前采用串行式的标记-清除法,每次GC都需要STW(stop the world),
也就是将所有用户程序暂停运行,cpu全力运行垃圾回收,进行标记和清除,程序很容易出现卡顿
go 1.5 为了降低GC延迟,采用了并发的标记和清除的三色标记法,
加入写屏障,实现了更好的回收器调度
go 1.8 引入混合屏障以消除STW中的re-scan,降低了STW的最差耗时
mheap.go
// 将 span放回 heap
func (h *mheap) freeSpan(s *mspan) {
systemstack(func() {
lock(&h.lock)
if msanenabled {
// 告诉 msan整个 span不再使用
base := unsafe.Pointer(s.base())
bytes := s.npages << _PageShift
msanfree(base, bytes)
}
if asanenabled {
// 告诉 asan整个 span不再使用
base := unsafe.Pointer(s.base())
bytes := s.npages << _PageShift
asanpoison(base, bytes)
}
h.freeSpanLocked(s, spanAllocHeap)
unlock(&h.lock)
})
}
func (h *mheap) freeSpanLocked(s *mspan, typ spanAllocType) {
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
// Free the span structure. We no longer have a use for it
s.state.set(mSpanDead)
h.freeMSpanLocked(s)
}
func (h *mheap) freeMSpanLocked(s *mspan) {
assertLockHeld(&h.lock)
pp := getg().m.p.ptr()
// 首先尝试将 mspan直接释放到缓存
if pp != nil && pp.mspancache.len < len(pp.mspancache.buf) {
pp.mspancache.buf[pp.mspancache.len] = s
pp.mspancache.len++
return
}
// 如果失败,就把它释放到 heap
h.spanalloc.free(unsafe.Pointer(s))
}
释放
在运行的入口函数main.main,会专门启动一个监控任务 sysmon,每隔一段时间就会检查 heap里闲置的内存块
如果闲置时间超过了阈值,则释放关联的物理内存
这里 Windows和 Linux有一点不同
mem_linux.go
func sysUnused(v unsafe.Pointer, n uintptr) {
if errno := madvise(v, n, int32(advise)); advise == _MADV_FREE && errno != 0 {
atomic.Store(&adviseUnused, _MADV_DONTNEED)
madvise(v, n, _MADV_DONTNEED)
}
调用 madvise
告知操作系统某段内存暂时不使用,建议内核回收对应的物理内存
这只是一个建议
,是否回收由内核决定,如果物理内存充足则忽略这个建议,避免不必要的消耗
当再次使用该内存块时,会引发缺页异常,内核会自动重新关联物理内存页
分配器面对的是虚拟内存,所以在地址空间充足下,根本无需放弃这段虚拟内存,无需收回 mspan,这也是 arena能线性扩张的根本原因
men_windows.go
func sysUnused(v unsafe.Pointer, n uintptr) {
for n > 0 {
small := n
for small >= 4096 && stdcall3(_VirtualFree, uintptr(v), small, _MEM_DECOMMIT) == 0 {
small /= 2
small &^= 4096 - 1
}
if small < 4096 {
print("runtime: VirtualFree of ", small, " bytes failed with errno=", getlasterror())
throw("runtime: failed to decommit pages")
}
v = add(v, small)
n -= small
}
}
Windows 则不支持 madvise 机制,需要在获取 span时主动补上被 VirtualFree掉的内存