6

详解Go中内存分配

 3 years ago
source link: http://www.cnblogs.com/luozhiyun/p/14349331.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

转载请声明出处哦~,本篇文章发布于luozhiyun的博客: https://www.luozhiyun.com

本文使用的go的源码15.7

介绍

Go 语言的内存分配器就借鉴了 TCMalloc 的设计实现高速的内存分配,它的核心理念是使用多级缓存将对象根据大小分类,并按照类别实施不同的分配策略。TCMalloc 相关的信息可以看这里: http://goog-perftools.sourceforge.net/doc/tcmalloc.html。

即如果要分配的对象是个小对象(<= 32k),在每个线程中都会有一个无锁的小对象缓存,可以直接高效的无锁的方式进行分配;

如下:对象被分到不同的内存大小组中的链表中。

UVj6FjI.png!mobile

如果是个大对象(>32k),那么页堆进行分配。如下:

qaEJnyy.png!mobile

虽然go内存分配器最初是基于tcmalloc的,但是现在已经有了很大的不同。所以上面的一些结构会有些许变化,下面再慢慢絮叨。

因为内存分配的源码比较复杂,为了方便大家调试,所以在进行源码分析之前,先看看是如何断点汇编来进行调试的。

断点调试汇编

目前Go语言支持GDB、LLDB和Delve几种调试器。只有Delve是专门为Go语言设计开发的调试工具。而且Delve本身也是采用Go语言开发,对Windows平台也提供了一样的支持。本节我们基于Delve简单解释如何调试Go汇编程序。项目地址: https://github.com/go-delve/delve

安装:

go get github.com/go-delve/delve/cmd/dlv

首先编写一个test.go的一个例子:

package main

import "fmt"

type A struct {
	test string
}
func main() {
	a := new(A)
	fmt.Println(a)
}

然后命令行进入包所在目录,然后输入 dlv debug 命令进入调试:

PS C:\document\code\test_go\src> dlv debug
Type 'help' for list of commands.

然后可以使用break命令在main包的main方法上设置一个断点:

(dlv) break main.main
Breakpoint 1 set at 0x4bd30a for main.main() c:/document/code/test_go/src/test.go:8

通过breakpoints查看已经设置的所有断点:

(dlv) breakpoints
Breakpoint runtime-fatal-throw at 0x4377e0 for runtime.fatalthrow() c:/software/go/src/runtime/panic.go:1162 (0)
Breakpoint unrecovered-panic at 0x437860 for runtime.fatalpanic() c:/software/go/src/runtime/panic.go:1189 (0)
        print runtime.curg._panic.arg
Breakpoint 1 at 0x4bd30a for main.main() c:/document/code/test_go/src/test.go:8 (0)

通过continue命令让程序运行到下一个断点处:

(dlv) continue
> main.main() c:/document/code/test_go/src/test.go:8 (hits goroutine(1):1 total:1) (PC: 0x4bd30a)
     3: import "fmt"
     4:
     5: type A struct {
     6:         test string
     7: }
=>   8: func main() {
     9:         a := new(A)
    10:         fmt.Println(a)
    11: }
    12:
    13:

通过disassemble反汇编命令查看main函数对应的汇编代码:

(dlv) disassemble
TEXT main.main(SB) C:/document/code/test_go/src/test.go
        test.go:8       0x4bd2f0        65488b0c2528000000      mov rcx, qword ptr gs:[0x28]
        test.go:8       0x4bd2f9        488b8900000000          mov rcx, qword ptr [rcx]
        test.go:8       0x4bd300        483b6110                cmp rsp, qword ptr [rcx+0x10]
        test.go:8       0x4bd304        0f8697000000            jbe 0x4bd3a1
=>      test.go:8       0x4bd30a*       4883ec78                sub rsp, 0x78
        test.go:8       0x4bd30e        48896c2470              mov qword ptr [rsp+0x70], rbp
        test.go:8       0x4bd313        488d6c2470              lea rbp, ptr [rsp+0x70]
        test.go:9       0x4bd318        488d0581860100          lea rax, ptr [__image_base__+874912]
        test.go:9       0x4bd31f        48890424                mov qword ptr [rsp], rax
        test.go:9       0x4bd323        e8e800f5ff              call $runtime.newobject
        test.go:9       0x4bd328        488b442408              mov rax, qword ptr [rsp+0x8]
        test.go:9       0x4bd32d        4889442430              mov qword ptr [rsp+0x30], rax
        test.go:10      0x4bd332        4889442440              mov qword ptr [rsp+0x40], rax
        test.go:10      0x4bd337        0f57c0                  xorps xmm0, xmm0
        test.go:10      0x4bd33a        0f11442448              movups xmmword ptr [rsp+0x48], xmm0
        test.go:10      0x4bd33f        488d442448              lea rax, ptr [rsp+0x48]
        test.go:10      0x4bd344        4889442438              mov qword ptr [rsp+0x38], rax
        test.go:10      0x4bd349        8400                    test byte ptr [rax], al
        test.go:10      0x4bd34b        488b4c2440              mov rcx, qword ptr [rsp+0x40]
        test.go:10      0x4bd350        488d15099f0000          lea rdx, ptr [__image_base__+815712]
        test.go:10      0x4bd357        4889542448              mov qword ptr [rsp+0x48], rdx
        test.go:10      0x4bd35c        48894c2450              mov qword ptr [rsp+0x50], rcx
        test.go:10      0x4bd361        8400                    test byte ptr [rax], al
        test.go:10      0x4bd363        eb00                    jmp 0x4bd365
        test.go:10      0x4bd365        4889442458              mov qword ptr [rsp+0x58], rax
        test.go:10      0x4bd36a        48c744246001000000      mov qword ptr [rsp+0x60], 0x1
        test.go:10      0x4bd373        48c744246801000000      mov qword ptr [rsp+0x68], 0x1
        test.go:10      0x4bd37c        48890424                mov qword ptr [rsp], rax
        test.go:10      0x4bd380        48c744240801000000      mov qword ptr [rsp+0x8], 0x1
        test.go:10      0x4bd389        48c744241001000000      mov qword ptr [rsp+0x10], 0x1
        test.go:10      0x4bd392        e869a0ffff              call $fmt.Println
        test.go:11      0x4bd397        488b6c2470              mov rbp, qword ptr [rsp+0x70]
        test.go:11      0x4bd39c        4883c478                add rsp, 0x78
        test.go:11      0x4bd3a0        c3                      ret
        test.go:8       0x4bd3a1        e82a50faff              call $runtime.morestack_noctxt
        .:0             0x4bd3a6        e945ffffff              jmp $main.main

现在我们可以使用break断点到runtime.newobject函数的调用上:

(dlv) break runtime.newobject
Breakpoint 2 set at 0x40d426 for runtime.newobject() c:/software/go/src/runtime/malloc.go:1164

输入continue跳到断点的位置:

(dlv) continue
> runtime.newobject() c:/software/go/src/runtime/malloc.go:1164 (hits goroutine(1):1 total:1) (PC: 0x40d426)
Warning: debugging optimized function
  1159: }
  1160:
  1161: // implementation of new builtin
  1162: // compiler (both frontend and SSA backend) knows the signature
  1163: // of this function
=>1164: func newobject(typ *_type) unsafe.Pointer {
  1165:         return mallocgc(typ.size, typ, true)
  1166: }
  1167:
  1168: //go:linkname reflect_unsafe_New reflect.unsafe_New
  1169: func reflect_unsafe_New(typ *_type) unsafe.Pointer {

print命令来查看typ的数据:

(dlv) print typ
*runtime._type {size: 16, ptrdata: 8, hash: 875453117, tflag: tflagUncommon|tflagExtraStar|tflagNamed (7), align: 8, fieldAlign: 8, kind: 25, equal: runtime.strequal, gcdata: *1, str: 5418, ptrToThis: 37472}

可以看到这里打印的size是16bytes,因为我们A结构体里面就一个string类型的field。

进入到mallocgc方法后,通过args和locals命令查看函数的参数和局部变量:

(dlv) args
size = (unreadable could not find loclist entry at 0x8b40 for address 0x40ca73)
typ = (*runtime._type)(0x4d59a0)
needzero = true
~r3 = (unreadable empty OP stack)
(dlv) locals
(no locals)

各个对象入口

我们根据汇编可以判断,所有的函数入口都是 runtime.mallocgc ,但是下面两个对象需要注意一下:

int64对象

runtime.convT64

func convT64(val uint64) (x unsafe.Pointer) {
	if val < uint64(len(staticuint64s)) {
		x = unsafe.Pointer(&staticuint64s[val])
	} else {
		x = mallocgc(8, uint64Type, false)
		*(*uint64)(x) = val
	}
	return
}

这段代码表示如果一个int64类型的值小于256,直接十三姨的是缓存值,那么这个值不会进行内存分配。

string对象

runtime.convTstring

func convTstring(val string) (x unsafe.Pointer) {
	if val == "" {
		x = unsafe.Pointer(&zeroVal[0])
	} else {
		x = mallocgc(unsafe.Sizeof(val), stringType, true)
		*(*string)(x) = val
	}
	return
}

由这段代码显示,如果是创建一个为”“的string对象,那么会直接返回一个固定的地址值,不会进行内存分配。

分析

分配器的组件

内存分配是由内存分配器完成,分配器由3种组件构成: runtime.mspanruntime.mcacheruntime.mcentralruntime.mheap

runtime.mspan

type mspan struct {
	// 上一个节点
	next *mspan     
	// 下一个节点
	prev *mspan      
	// span集合
	list *mSpanList  
	// span开始的地址值
	startAddr uintptr  
	// span管理的页数
	npages    uintptr  
 
	// Object n starts at address n*elemsize + (start << pageShift).
	// 空闲节点的索引
	freeindex uintptr 
	// span中存放的对象数量
	nelems uintptr  
 
	// 用于快速查找内存中未被使用的内存
	allocCache uint64 
  // 用于计算mspan管理了多少内存
  elemsize    uintptr
  // span的结束地址值
  limit       uintptr
  
	...
}

runtime.mspan 是内存管理器里面的最小粒度单元,所有的对象都是被管理在mspan下面。

mspan是一个链表,有上下指针;

npages代表mspan管理的堆页的数量;

freeindex是空闲对象的索引;

nelems代表这个mspan中可以存放多少对象,等于 (npages * pageSize)/elemsize

allocCache用于快速的查找未被使用的内存地址;

elemsize表示一个对象会占用多个个bytes,等于 class_to_size[sizeclass] ,需要注意的是sizeclass每次获取的时候会sizeclass方法,将 sizeclass>>1

limit表示span结束的地址值,等于 startAddr+ npages*pageSize

实例图如下:

R77RZf.png!mobile

图中alloc是一个拥有137个元素的mspan数组,mspan数组管理数个page大小的内存,每个page是8k,page的数量由spanclass规格决定。

runtime.mcache

type mcache struct { 
	...
	// 申请小对象的起始地址
	tiny             uintptr
	// 从起始地址tiny开始的偏移量
	tinyoffset       uintptr
	// tiny对象分配的数量
	local_tinyallocs uintptr // number of tiny allocs not counted in other stats
	// mspan对象集合,numSpanClasses=134
	alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
	...
}

runtime.mcache 是绑在并发模型GPM的P上,在分配微对象和小对象的时候会先去 runtime.mcache 中获取,每一个处理器都会被分配一个线程缓存 runtime.mcache ,因此从 runtime.mcache 进行分配时无需加锁。

runtime.mcache 中有一个alloc数组,是 runtime.mspan 的集合, runtime.mspan 是 Go 语言内存管理的基本单元。对于[16B,32KB]的对象会使用这部分span进行内存分配,所以所有在这区间大小的对象都会从alloc这个数组里寻找,下面会分析到。

runtime.mcentral

type mcentral struct {
	lock      mutex
    //spanClass Id
	spanclass spanClass
    // 空闲的span列表
	nonempty  mSpanList // list of spans with a free object, ie a nonempty free list
	// 已经被使用的span列表
    empty     mSpanList // list of spans with no free objects (or cached in an mcache)

	//分配mspan的累积计数
	nmalloc uint64
}

type mSpanList struct {
    //链表头
	first *mspan // first span in list, or nil if none
    //链表尾部
	last  *mspan // last span in list, or nil if none
}

runtime.mcache 中空间不足的时候,会去 runtime.mcentral 中申请对应规格的mspan。由于由于 runtime.mcentral 是公共资源,会有多个 runtime.mcache 向它申请 runtime.mspan ,因此必须加锁。

runtime.mcentral 中,有spanclass标识,spanclass表示这个mcentral的类型,下面我们会看到,在分配[16B,32KB]大小对象的时候,会将对象的大小分成67组:

var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

所以 runtime.mcentral 只负责一种spanclass规格类型,该规格的所有未被使用的空闲mspan会挂载到nonempty 链表上,已经被mcache拿走,未归还的会挂载到empty 链表上,归还后会再挂载到nonempty上。mspan会以链表的形式链接在 runtime.mcentral 上面。

viI3mi6.png!mobile

runtime.mheap

type mheap struct { 
	lock      mutex
	pages     pageAlloc // page allocation data structure 
	
	//arenas数组集合,一个二维数组
	arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena

	//各个规格的mcentral集合
	central [numSpanClasses]struct {
		mcentral mcentral
		pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
	}
	...
}

对于 runtime.mheap 需要关注central和arenas。central是各个规格的mcentral集合,在初始化的时候会通过遍历class_to_size来进行创建;arenas是一个二维数组,用来管理内存空间。arenas由多个 runtime.heapArena 组成,每个单元都会管理 64MB 的内存空间:

const (
	pageSize             = 8192                       // 8KB
	heapArenaBytes       = 67108864                   // 64MB 
	pagesPerArena        = heapArenaBytes / pageSize  // 8192
)

type heapArena struct {
	bitmap [heapArenaBitmapBytes]byte
	spans [pagesPerArena]*mspan
	pageInUse [pagesPerArena / 8]uint8
	pageMarks [pagesPerArena / 8]uint8
	zeroedBase uintptr
}

需要注意的是,上面的heapArenaBytes代表的64M只是在除windows以外的64 位机器才会显示,在windows机器上显示的是4MB。具体的可以看下面的官方注释:

//       Platform  Addr bits  Arena size  L1 entries   L2 entries
	// --------------  ---------  ----------  ----------  -----------
	//       */64-bit         48        64MB           1    4M (32MB)
	// windows/64-bit         48         4MB          64    1M  (8MB)
	//       */32-bit         32         4MB           1  1024  (4KB)
	//     */mips(le)         31         4MB           1   512  (2KB)

L1 entries、L2 entries分别代表的是 runtime.mheap 中arenas一维、二维的值。

In2ANzZ.png!mobile

给对象分配内存

我们通过对源码的反编译可以知道,堆上所有的对象都会通过调用 runtime.newobject 函数分配内存,该函数会调用 runtime.mallocgc :

//创建一个新的对象
func newobject(typ *_type) unsafe.Pointer {
    //size表示该对象的大小
	return mallocgc(typ.size, typ, true)
}

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { 
	...  
	dataSize := size
	// 获取mcache,用于处理微对象和小对象的分配
	c := gomcache()
	var x unsafe.Pointer
	// 表示对象是否包含指针,true表示对象里没有指针
	noscan := typ == nil || typ.ptrdata == 0
	// maxSmallSize=32768 32k
	if size <= maxSmallSize {
		// maxTinySize= 16 bytes 
		if noscan && size < maxTinySize {
			...
		} else {
			...
		}
		// 大于 32 Kb 的内存分配,通过 mheap 分配
	} else {
		...
	} 
	... 
	return x
}

通过mallocgc的代码可以知道,mallocgc在分配内存的时候,会按照对象的大小分为3档来进行分配:

  1. 小于16bytes的小对象;
  2. 在16bytes与32k之间的微对象;
  3. 大于 32 Kb的大对象;

大对象分配

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { 
	...  
	var s *mspan
	shouldhelpgc = true
	systemstack(func() {
		s = largeAlloc(size, needzero, noscan)
	})
	s.freeindex = 1
	s.allocCount = 1
	x = unsafe.Pointer(s.base())
	size = s.elemsize
	... 
	return x
}

从上面我们可以看到分配大于32KB的空间时,直接使用largeAlloc来分配一个mspan。

func largeAlloc(size uintptr, needzero bool, noscan bool) *mspan {
	// _PageSize=8k,也就是表明对象太大,溢出
	if size+_PageSize < size {
		throw("out of memory")
	}
	// _PageShift==13,计算需要分配的页数
	npages := size >> _PageShift
	// 如果不是整数,多出来一些,需要加1
	if size&_PageMask != 0 {
		npages++
	} 
	...
	// 从堆上分配
	s := mheap_.alloc(npages, makeSpanClass(0, noscan), needzero)
	if s == nil {
		throw("out of memory")
	}
	...
	return s
}

在分配内存的时候是按页来进行分配的,每个页的大小是_PageSize(8K),然后需要根据传入的size来判断需要分多少页,最后调用alloc从堆上分配。

func (h *mheap) alloc(npages uintptr, spanclass spanClass, needzero bool) *mspan {
	var s *mspan
	systemstack(func() { 
		if h.sweepdone == 0 {
			// 回收一部分内存
			h.reclaim(npages)
		}
		// 进行内存分配
		s = h.allocSpan(npages, false, spanclass, &memstats.heap_inuse)
	}) 
	...
	return s
}

继续看allocSpan的实现:

const pageCachePages = 8 * unsafe.Sizeof(pageCache{}.cache)

func (h *mheap) allocSpan(npages uintptr, manual bool, spanclass spanClass, sysStat *uint64) (s *mspan) {
	// Function-global state.
	gp := getg()
	base, scav := uintptr(0), uintptr(0)
 
	pp := gp.m.p.ptr()
	// 申请的内存比较小,尝试从pcache申请内存
	if pp != nil && npages < pageCachePages/4 {
		c := &pp.pcache
 
		if c.empty() {
			lock(&h.lock)
			*c = h.pages.allocToCache()
			unlock(&h.lock)
		} 

		base, scav = c.alloc(npages)
		if base != 0 {
			s = h.tryAllocMSpan()

			if s != nil && gcBlackenEnabled == 0 && (manual || spanclass.sizeclass() != 0) {
				goto HaveSpan
			} 
		}
	} 
	lock(&h.lock)
	// 内存比较大或者线程的页缓存中内存不足,从mheap的pages上获取内存
	if base == 0 { 
		base, scav = h.pages.alloc(npages)
		// 内存也不够,那么进行扩容
		if base == 0 {
			if !h.grow(npages) {
				unlock(&h.lock)
				return nil
			}
			// 重新申请内存
			base, scav = h.pages.alloc(npages)
			// 内存不足,抛出异常
			if base == 0 {
				throw("grew heap, but no adequate free space found")
			}
		}
	}
	if s == nil { 
		// 分配一个mspan对象
		s = h.allocMSpanLocked()
	}
 
	unlock(&h.lock)

HaveSpan: 
	// 设置参数初始化
	s.init(base, npages) 
	...
	// 建立mheap与mspan之间的联系
	h.setSpans(s.base(), npages, s)
	...
	return s
}

这里会根据需要分配的内存大小再判断一次:

pageCachePages/4=64/4=16
runtime.pageAlloc.alloc

下面来看看grow的向操作系统申请内存:

func (h *mheap) grow(npage uintptr) bool {
	// We must grow the heap in whole palloc chunks.
	ask := alignUp(npage, pallocChunkPages) * pageSize

	totalGrowth := uintptr(0)
	nBase := alignUp(h.curArena.base+ask, physPageSize)
	// 内存不够则调用sysAlloc申请内存
	if nBase > h.curArena.end { 
		av, asize := h.sysAlloc(ask)
		if av == nil {
			print("runtime: out of memory: cannot allocate ", ask, "-byte block (", memstats.heap_sys, " in use)\n")
			return false
		}
		// 重新设置curArena的值
		if uintptr(av) == h.curArena.end { 
			h.curArena.end = uintptr(av) + asize
		} else { 
			if size := h.curArena.end - h.curArena.base; size != 0 {
				h.pages.grow(h.curArena.base, size)
				totalGrowth += size
			} 
			h.curArena.base = uintptr(av)
			h.curArena.end = uintptr(av) + asize
		} 
		nBase = alignUp(h.curArena.base+ask, physPageSize)
	} 
	...
	return true
}

grow会通过curArena的end值来判断是不是需要从系统申请内存;如果end小于nBase那么会调用 runtime.mheap.sysAlloc 方法从操作系统中申请更多的内存;

func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {
	n = alignUp(n, heapArenaBytes)
 
	// 在预先保留的内存中申请一块可以使用的空间
	v = h.arena.alloc(n, heapArenaBytes, &memstats.heap_sys)
	if v != nil {
		size = n
		goto mapped
	} 
	// 根据页堆的arenaHints在目标地址上尝试扩容
	for h.arenaHints != nil {
		hint := h.arenaHints
		p := hint.addr
		if hint.down {
			p -= n
		}
		if p+n < p {
			// We can't use this, so don't ask.
			v = nil
		} else if arenaIndex(p+n-1) >= 1<<arenaBits {
			// Outside addressable heap. Can't use.
			v = nil
		} else {
			// 从操作系统中申请内存
			v = sysReserve(unsafe.Pointer(p), n)
		}
		if p == uintptr(v) {
			// Success. Update the hint.
			if !hint.down {
				p += n
			}
			hint.addr = p
			size = n
			break
		} 
		if v != nil {
			sysFree(v, n, nil)
		}
		h.arenaHints = hint.next
		h.arenaHintAlloc.free(unsafe.Pointer(hint))
	}  
	...
	// 将内存由Reserved转为Prepared
	sysMap(v, size, &memstats.heap_sys)

mapped:
	// Create arena metadata.
	// 初始化一个heapArena来管理刚刚申请的内存
	for ri := arenaIndex(uintptr(v)); ri <= arenaIndex(uintptr(v)+size-1); ri++ {
		l2 := h.arenas[ri.l1()]
		if l2 == nil { 
			l2 = (*[1 << arenaL2Bits]*heapArena)(persistentalloc(unsafe.Sizeof(*l2), sys.PtrSize, nil))
			if l2 == nil {
				throw("out of memory allocating heap arena map")
			}
			atomic.StorepNoWB(unsafe.Pointer(&h.arenas[ri.l1()]), unsafe.Pointer(l2))
		}  
		var r *heapArena
		r = (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys))
		...  
		// 将创建heapArena放入到arenas列表中
		h.allArenas = h.allArenas[:len(h.allArenas)+1]
		h.allArenas[len(h.allArenas)-1] = ri
		atomic.StorepNoWB(unsafe.Pointer(&l2[ri.l2()]), unsafe.Pointer(r))
	}
	return
}

sysAlloc方法会调用 runtime.linearAlloc.alloc 预先保留的内存中申请一块可以使用的空间;如果没有会调用sysReserve方法会从操作系统中申请内存;最后初始化一个heapArena来管理刚刚申请的内存,然后将创建heapArena放入到arenas列表中。

至此,大对象的分配流程至此结束。

小对象分配

对于介于16bytes~32K的对象分配如下:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	...
	dataSize := size
	// 获取mcache,用于处理微对象和小对象的分配
	c := gomcache()
	var x unsafe.Pointer
	// 表示对象是否包含指针,true表示对象里没有指针
	noscan := typ == nil || typ.ptrdata == 0
	// maxSmallSize=32768 32k
	if size <= maxSmallSize {
		// maxTinySize= 16 bytes 
		if noscan && size < maxTinySize { 
			...
		} else {
			var sizeclass uint8
			//计算 sizeclass
			// smallSizeMax=1024
			if size <= smallSizeMax-8 {
				// smallSizeDiv=8
				sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]
			} else {
				// largeSizeDiv=128,smallSizeMax = 1024
				sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]
			}
			size = uintptr(class_to_size[sizeclass])
			spc := makeSpanClass(sizeclass, noscan)
			span := c.alloc[spc]
			//从对应的 span 里面分配一个 object 
			v := nextFreeFast(span)
			if v == 0 {
				// mcache不够用了,则从 mcentral 申请内存到 mcache
				v, span, shouldhelpgc = c.nextFree(spc)
			}
			x = unsafe.Pointer(v)
			if needzero && span.needzero != 0 {
				memclrNoHeapPointers(unsafe.Pointer(v), size)
			}
		} 
		...
	}  
	...
	return x
}

首先会先计算sizeclass 大小,计算 sizeclass 是通过预先定义两个数组:size_to_class8 和 size_to_class128。小于 1024 - 8 = 1016 (smallSizeMax=1024),使用 size_to_class8,否则使用数组 size_to_class128。

举个例子,比如要分配 20 byte 的内存,那么sizeclass = size_to_calss8[(20+7)/8] = size_to_class8[3] = 3。然后通过class_to_size[3]获取到对应的值32,表示应该要分配32bytes的内存值。

接着会从alloc数组中获取一个span的指针,通过调用nextFreeFast尝试从mcache中获取内存,如果mcache不够用了,则尝试调用nextFree从 mcentral 申请内存到 mcache。

下面看看nextFreeFast:

func nextFreeFast(s *mspan) gclinkptr {
    // 获取allocCache二进制中0的个数
	theBit := sys.Ctz64(s.allocCache) // Is there a free object in the allocCache?
	if theBit < 64 {
		result := s.freeindex + uintptr(theBit)
		if result < s.nelems {
			freeidx := result + 1
			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
}

allocCache在初始化的时候会初始化成 ^uint64(0) ,换算成二进制,如果为0则表示被占用,通过allocCache可以快速的定位待分配的空间:

VvINzeM.png!mobile

func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
	s = c.alloc[spc]
	shouldhelpgc = false
	// 当前span中找到合适的index索引
	freeIndex := s.nextFreeIndex()
	// 当前span已经满了
	if freeIndex == s.nelems { 
		if uintptr(s.allocCount) != s.nelems {
			println("runtime: s.allocCount=", s.allocCount, "s.nelems=", s.nelems)
			throw("s.allocCount != s.nelems && freeIndex == s.nelems")
		}
		// 从 mcentral 中获取可用的span,并替换掉当前 mcache里面的span
		c.refill(spc)
		shouldhelpgc = true
		s = c.alloc[spc]
		// 再次到新的span里面查找合适的index
		freeIndex = s.nextFreeIndex()
	}

	if freeIndex >= s.nelems {
		throw("freeIndex is not valid")
	}
	// 计算出来内存地址,并更新span的属性
	v = gclinkptr(freeIndex*s.elemsize + s.base())
	s.allocCount++
	if uintptr(s.allocCount) > s.nelems {
		println("s.allocCount=", s.allocCount, "s.nelems=", s.nelems)
		throw("s.allocCount > s.nelems")
	}
	return
}

nextFree中会判断当前span是不是已经满了,如果满了就调用refill方法从 mcentral 中获取可用的span,并替换掉当前 mcache里面的span。

func (c *mcache) refill(spc spanClass) { 
	s := c.alloc[spc]
	...
	s = mheap_.central[spc].mcentral.cacheSpan()
	if s == nil {
		throw("out of memory")
	} 
	...
	c.alloc[spc] = s
}

Refill 根据指定的sizeclass获取对应的span,并作为 mcache的新的sizeclass对应的span。

func (c *mcentral) cacheSpan() *mspan {
	...
	sg := mheap_.sweepgen
	spanBudget := 100

	var s *mspan
 
	// 从清理过的、包含空闲空间的spanSet结构中查找可以使用的内存管理单元
	if s = c.partialSwept(sg).pop(); s != nil {
		goto havespan
	} 
	for ; spanBudget >= 0; spanBudget-- {
		// 从未被清理过的、有空闲对象的spanSet查找可用的span
		s = c.partialUnswept(sg).pop()
		if s == nil {
			break
		}
		if atomic.Load(&s.sweepgen) == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
			// 找到要回收的span,触发sweep进行清理
			s.sweep(true)
			goto havespan
		}
	}
	for ; spanBudget >= 0; spanBudget-- {
		// 获取未被清理的、不包含空闲空间的spanSet查找可用的span
		s = c.fullUnswept(sg).pop()
		if s == nil {
			break
		}
		if atomic.Load(&s.sweepgen) == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
			s.sweep(true)
			freeIndex := s.nextFreeIndex()
			if freeIndex != s.nelems {
				s.freeindex = freeIndex
				goto havespan
			}
			c.fullSwept(sg).push(s)
		}
	}
	// 从堆中申请新的内存管理单元
	s = c.grow()
	if s == nil {
		return nil
	} 
havespan:
	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")
	} 
	//更新 nmalloc
	atomic.Xadd64(&c.nmalloc, int64(n))
	usedBytes := uintptr(s.allocCount) * s.elemsize
	atomic.Xadd64(&memstats.heap_live, int64(spanBytes)-int64(usedBytes))
	if trace.enabled {
		// heap_live changed.
		traceHeapAlloc()
	}
	if gcBlackenEnabled != 0 {
		// heap_live changed.
		gcController.revise()
	}
	freeByteBase := s.freeindex &^ (64 - 1)
	whichByte := freeByteBase / 8 
	// 更新allocCache
	s.refillAllocCache(whichByte) 
 	// s.allocCache.
	s.allocCache >>= s.freeindex % 64 
	return s
}

cacheSpan主要是从mcentral的spanset中去寻找可用的span,如果没找到那么调用grow方法从堆中申请新的内存管理单元。

获取到后更新nmalloc、allocCache等字段。

runtime.mcentral.grow 触发扩容操作从堆中申请新的内存:

func (c *mcentral) grow() *mspan {
	// 获取待分配的页数
	npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
	size := uintptr(class_to_size[c.spanclass.sizeclass()])
	// 获取新的span
	s := mheap_.alloc(npages, c.spanclass, true)
	if s == nil {
		return nil
	}

	// Use division by multiplication and shifts to quickly compute:
	// n := (npages << _PageShift) / size
	n := (npages << _PageShift) >> s.divShift * uintptr(s.divMul) >> s.divShift2
	// 初始化limit 
	s.limit = s.base() + size*n
	heapBitsForAddr(s.base()).initSpan(s)
	return s
}

grow里面会调用 runtime.mheap.alloc 方法获取span,这个方法在上面已经讲过了,不记得的同学可以翻一下文章上面。

到这里小对象的分配就讲解完毕了。

微对象分配

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	...
	dataSize := size
	// 获取mcache,用于处理微对象和小对象的分配
	c := gomcache()
	var x unsafe.Pointer
	// 表示对象是否包含指针,true表示对象里没有指针
	noscan := typ == nil || typ.ptrdata == 0
	// maxSmallSize=32768 32k
	if size <= maxSmallSize {
		// maxTinySize= 16 bytes 
		if noscan && size < maxTinySize { 
			off := c.tinyoffset 
			// 指针内存对齐
			if size&7 == 0 {
				off = alignUp(off, 8)
			} else if size&3 == 0 {
				off = alignUp(off, 4)
			} else if size&1 == 0 {
				off = alignUp(off, 2)
			}
			// 判断指针大小相加是否超过16
			if off+size <= maxTinySize && c.tiny != 0 {
				// 获取tiny空闲内存的起始位置
				x = unsafe.Pointer(c.tiny + off)
				// 重设偏移量
				c.tinyoffset = off + size
				// 统计数量
				c.local_tinyallocs++
				mp.mallocing = 0
				releasem(mp)
				return x
			}  
			// 重新分配一个内存块
			span := c.alloc[tinySpanClass]
			v := nextFreeFast(span)
			if v == 0 {
				v, _, shouldhelpgc = c.nextFree(tinySpanClass)
			}
			x = unsafe.Pointer(v)
			//将申请的内存块全置为 0
			(*[2]uint64)(x)[0] = 0
			(*[2]uint64)(x)[1] = 0 
			// 如果申请的内存块用不完,则将剩下的给 tiny,用 tinyoffset 记录分配了多少。
			if size < c.tinyoffset || c.tiny == 0 {
				c.tiny = uintptr(x)
				c.tinyoffset = size
			}
			size = maxTinySize
		}  
		...
	}  
	...
	return x
}

在分配对象内存的时候做了一个判断, 如果该对象的大小小于16bytes,并且是不包含指针的,那么就可以看作是微对象。

在分配微对象的时候,会先判断一下tiny指向的内存块够不够用,如果tiny剩余的空间超过了size大小,那么就直接在tiny上分配内存返回;

vM3QvqR.png!mobile

这里我再次使用我上面的图来加以解释。首先会去mcache数组里面找到对应的span,tinySpanClass对应的span的属性如下:

startAddr: 824635752448,
npages: 1,
manualFreeList: 0,
freeindex: 128,
nelems: 512,
elemsize: 16,
limit: 824635760640,
allocCount: 128,
spanclass: tinySpanClass (5),
...

tinySpanClass对应的mspan里面只有一个page,里面的元素可以装512(nelems)个;page里面每个对象的大小是16bytes(elemsize),目前已分配了128个对象(allocCount),当然我上面的page画不了这么多,象征性的画了一下。

上面的图中还画了在page里面其中的一个object已经被使用了12bytes,还剩下4bytes没有被使用,所以会更新tinyoffset与tiny的值。

总结

本文先是介绍了如何对go的汇编进行调试,然后分了三个层次来讲解go中的内存分配是如何进行的。对于小于32k的对象来说,go通过无锁的方式可以直接从mcache获取到了对应的内存,如果mcache内存不够的话,先是会到mcentral中获取内存,最后才到mheap中申请内存。对于大对象(>32k)来说可以直接mheap中申请,但是对于大对象来说也是有一定优化,当大对象需要分配的页小于16页的时候会直接从pageCache中分配,否则才会从堆页中获取。

Reference

https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-09-debug.html

https://deepu.tech/memory-management-in-golang/

https://medium.com/@ankur_anand/a-visual-guide-to-golang-memory-allocator-from-ground-up-e132258453ed

http://goog-perftools.sourceforge.net/doc/tcmalloc.html

https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK