Golang中内存分配与管理机制

mspan

  • page:最小存储单元

    Golang中的页的大小为8KB,Go中是以页为单位向操作系统申请内存的。

  • mspan:为最小的管理单元

    mspan的大小为page的整数倍,从8B到80KB划分成了67个不同的规格,分配对象的时候,会从按照对象的大小从不同规格的mspan中获取空间

    所以mspan可以看成是一个或多个连续的页。

    优点:

    1. 消除了外部碎片,但是会产生内部碎片
    2. 使mcentral支持实现更加细致的锁

    同等级的mspan属于同一个mcentral,所以会基于同一把互斥锁

    mspan会基于bitMap辅助快速找到空闲内存块(Ctz64算法)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
    
    // freeindex之前的位置都被占用了
	freeindex uintptr
    // 最多可以存放多少个object
	nelems uintptr // number of object in the span.
    
    // ☆ allocaCache中每个bit对应一个object块,标识该块是否被占用
	allocCache uint64
	allocBits  *gcBits
    
	gcmarkBits *gcBits
	sweepgen              uint32
	divMul                uint32        // for divide by elemsize
	allocCount            uint16        // number of allocated objects
    
    // 表示mspan对应的那67个等级中的哪一个
	spanclass             spanClass     // size class and noscan (uint8)
    
	state                 mSpanStateBox // mSpanInUse etc; accessed atomically (get/set methods)
	needzero              uint8         // needs to be zeroed before allocation
	allocCountBeforeCache uint16        // a copy of allocCount that is stored just before this span is cached
	elemsize              uintptr       // computed from sizeclass or from npages
	limit                 uintptr       // end of data in span
	speciallock           mutex         // guards specials list
	specials              *special      // linked list of special records sorted by offset.
}
  • next/prev:指的是后一个和前一个Span,说明Span是一个双向链表
  • startAddr:当前Span的起始地址。
  • npages:当前Span中一共含有多少页
  • nelems:当前Span中存储的对象的个数
  • spanclass:当前Span的等级

spanClass

Go语言将内存分成了67个级别,其中0代表大小不固定的大对象,普通对象在分配内存的时候是更具下面这个表来按照最合适的大小来分配的:

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
5 48 8192 170 32 31.52% 16
6 64 8192 128 0 23.44% 64
7 80 8192 102 32 19.07% 16
8 96 8192 85 32 15.95% 32
9 112 8192 73 16 13.56% 16
10 128 8192 64 0 11.72% 128
11 144 8192 56 128 11.82% 16
12 160 8192 51 32 9.73% 32
13 176 8192 46 96 9.59% 16
14 192 8192 42 128 9.25% 64
15 208 8192 39 80 8.12% 16
16 224 8192 36 128 8.15% 32
17 240 8192 34 32 6.62% 16
18 256 8192 32 0 5.86% 256
19 288 8192 28 128 12.16% 32
20 320 8192 25 192 11.80% 64
21 352 8192 23 96 9.88% 32
22 384 8192 21 128 9.51% 128
23 416 8192 19 288 10.71% 32
24 448 8192 18 128 8.37% 64
25 480 8192 17 32 6.82% 32
26 512 8192 16 0 6.05% 512
27 576 8192 14 128 12.33% 64
28 640 8192 12 512 15.48% 128
29 704 8192 11 448 13.93% 64
30 768 8192 10 512 13.94% 256
31 896 8192 9 128 15.52% 128
32 1024 8192 8 0 12.40% 1024
33 1152 8192 7 128 12.41% 128
34 1280 8192 6 512 15.55% 256
35 1408 16384 11 896 14.00% 128
36 1536 8192 5 512 14.00% 512
37 1792 16384 9 256 15.57% 256
38 2048 8192 4 0 12.45% 2048
39 2304 16384 7 256 12.46% 256
40 2688 8192 3 128 15.59% 128
41 3072 24576 8 0 12.47% 1024
42 3200 16384 5 384 6.22% 128
43 3456 24576 7 384 8.83% 128
44 4096 8192 2 0 15.60% 4096
45 4864 24576 5 256 16.65% 256
46 5376 16384 3 256 10.92% 256
47 6144 24576 4 0 12.48% 2048
48 6528 32768 5 128 6.23% 128
49 6784 40960 6 256 4.36% 128
50 6912 49152 7 768 3.37% 256
51 8192 8192 1 0 15.61% 8192
52 9472 57344 6 512 14.28% 256
53 9728 49152 5 512 3.64% 512
54 10240 40960 4 0 4.99% 2048
55 10880 32768 3 128 6.24% 128
56 12288 24576 2 0 11.45% 4096
57 13568 40960 3 256 9.99% 256
58 14336 57344 4 0 5.35% 2048
59 16384 16384 1 0 12.49% 8192
60 18432 73728 4 0 11.11% 2048
61 19072 57344 3 128 3.57% 128
62 20480 40960 2 0 6.87% 4096
63 21760 65536 3 256 6.25% 256
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

上面的数据在runtime/sizeclasses.go中有记载。

  1. class:span的等级
  2. bytes/obj:该级别的Span中对象的最大大小,比如说第一级别中代表对象的大小只能是:1~8个字节。第二级别代表对象的大小只能是9~16个字节。
  3. objects:该级别的Span中最多能存储多少个对象。
  4. tail waste:尾部浪费。即对象的大小没有达到该级别的最大的大小而导致的内部碎片的浪费。
  5. max waste:最大浪费率

如果一个对象的大小是17个byte,则会分配给class为3的mspan。如果一个对象的大小为36,则会分配给class为5的mspan。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// spanClass 表示 span 的大小类别和无扫描(noscan)属性。
//
// 每个大小类别都有一个 noscan spanClass 和一个 scan spanClass。
// noscan spanClass 仅包含 noscan 对象,这些对象不包含指针,
// 因此不需要由垃圾收集器扫描。
type spanClass uint8


// makeSpanClass 根据大小类别(sizeclass)和无扫描(noscan)属性创建 spanClass。
// sizeclass 是一个表示大小类别的 uint8 值。
// noscan 是一个布尔值,表示该 spanClass 是否为 noscan。
func makeSpanClass(sizeclass uint8, noscan bool) spanClass {
	// 将 sizeclass 左移 1 位,然后将 noscan 转换为整数后做按位或运算,生成最终的 spanClass。
	return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
}


// 获取当前的等级
func (sc spanClass) sizeclass() int8 {
	return int8(sc >> 1)
}

// 是否是noscan
func (sc spanClass) noscan() bool {
	return sc&1 != 0
}

spanClass是uint8类型的,其中高7位用于表示span的等级,最低位表示noscan,noscan表示了object中是否包含指针,在gc的时候有用。

mcache

线程级别。

src/runtime/mcache.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const (
    // 136
	numSpanClasses = _NumSizeClasses << 1
	tinySpanClass  = spanClass(tinySizeClass<<1 | 1)
)


// 每个线程(在 Go 中,每个 P)的缓存用于小对象。
// 包含一个小对象缓存和本地分配统计。
// 由于它是每个线程(每个 P)的,所以不需要锁定。
//
// mcaches 是从非 GC 管理的内存中分配的,因此任何堆指针必须特别处理。
//
//go:notinheap
type mcache struct {
	// 以下成员在每次 malloc 时都会被访问,
	// 因此将它们放在一起以便更好地缓存。
	nextSample uintptr // 在分配了这么多字节之后触发堆采样
	scanAlloc  uintptr // 分配的可扫描堆的字节数

	// 【没有指针的小对象】的分配器缓存。
	// 请参见 malloc.go 中的 "Tiny allocator" 注释。

	// tiny 指向当前 tiny 块的开头,
	// 如果没有当前 tiny 块,则为 nil。
	//
	// tiny 是一个堆指针。由于 mcache 位于非 GC 管理的内存中,
	// 我们通过在标记终止期间在 releaseAll 中清除它来处理它。
	//
	// tinyAllocs 是拥有此 mcache 的 P 执行的 tiny 分配的次数。
    // 指向堆中的一片内存
	tiny       uintptr
    // 下一个空闲内存所在的偏移量
	tinyoffset uintptr
    // 分配的次数
	tinyAllocs uintptr

	// 其余部分在每次 malloc 时不会被访问。
	alloc [numSpanClasses]*mspan // 根据 spanClass 索引分配用的 span

	stackcache [_NumStackOrders]stackfreelist // 用于缓存栈的空闲列表

	// flushGen 表示上次刷新此 mcache 的 sweepgen。
	// 如果 flushGen != mheap_.sweepgen,则此 mcache 中的 span 是陈旧的,
	// 需要刷新以便可以被清扫。这在 acquirep 中完成。
	flushGen uint32
}
  • 是Go语言中的线程缓存,他会与线程上的处理器P绑定,主要用来缓存用户程序申请的微小对象。

  • 每个线程缓存都持有136 (68(大小维度) * 2(noscan维度))个mspan,位于字段alloc

    img

  • mcache中有一个tiny allocator

    • tiny:指向堆中的一片内存
    • tinyoffset: 下一个空闲内存所在的偏移量
    • tinyAllocs:分配的次数
    • 用于处理小于16B的对象
    • 只会用于分配非指针类型的对象

mcentral

src\runtime\mcentral.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 给定大小的空闲对象的中央列表。
//
//go:notinheap
type mcentral struct {
	spanclass spanClass

	// partial 和 full 包含两组 mspan 集合:一组是已经清扫的在使用的 span,
	// 另一组是尚未清扫的在使用的 span。这两组在每个 GC 周期中交换角色。
	// 未清扫集合在每个 GC 周期中要么通过分配要么通过后台清扫器被清空,
	// 因此只需要两组。
	//
	// sweepgen 在每个 GC 周期中增加 2,因此清扫过的 span 位于
	// partial[sweepgen/2%2] 中,而未清扫的 span 位于
	// partial[1-sweepgen/2%2] 中。清扫会从未清扫集合中弹出 span,
	// 并将仍在使用的 span 推入清扫过的集合。同样,分配一个在使用的 span
	// 会将其推入清扫过的集合。
	//
	// 清扫器的某些部分可以清扫任意 span,因此不能从未清扫集合中移除它们,
	// 但会将 span 添加到适当的清扫列表中。因此,从未清扫列表中消耗的
	// 清扫器和 mcentral 部分可能会遇到清扫过的 span,这些应被忽略。
	partial [2]spanSet // 有空位的mspan集合,数组长度为2是为了抗一轮GC
	full    [2]spanSet // 没有空位的mspan集合
}
  • 每个mcentral对应一中spanClass,
  • 每个mcentral下聚合了该spanClass

mheap

全局堆缓存

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// 主堆分配器。
// 堆本身是 "free" 和 "scav" treaps,但所有其他全局数据也在这里。
//
// mheap 不能堆分配,因为它包含 mSpanLists,这些不能堆分配。
//
//go:notinheap
type mheap struct {
	// lock 只能在系统栈上获取,否则如果 g 的栈在持有锁时增长,可能会自死锁。
	lock mutex

	_ uint32 // 8字节对齐 pages,以使其与测试对齐。

    // 空闲页分配器,底层是多个基数树组成的索引,每棵树对应的空间为16GB
	pages pageAlloc // 页分配数据结构

	sweepgen uint32 // 清扫代,见 mspan 中的注释;在 STW(stop-the-world)期间写入

	// allspans 是所有曾经创建的 mspan 的切片。每个 mspan 只出现一次。
	//
	// allspans 的内存是手动管理的,并且可以随着堆的增长重新分配和移动。
	//
	// 通常,allspans 由 mheap_.lock 保护,这可以防止并发访问和释放底层存储。
	// 在 STW 期间的访问可能不会持有锁,但必须确保在访问时不能发生分配
	// (因为这可能会释放底层存储)。
    // 记录了所有的 mspan. 需要知道,所有 mspan 都是经由 mheap,使用连续空闲页组装生成的
	allspans []*mspan // 所有的 span

	// Proportional sweep(比例清扫)
	//
	// 这些参数表示一个从 gcController.heapLive 到页面清扫计数的线性函数。
	// 比例清扫系统通过保持当前页面清扫计数高于当前 gcController.heapLive
	// 时的这条线来维持在黑色。
	//
	// 该线的斜率为 sweepPagesPerByte,并且通过一个基点 (sweepHeapLiveBasis,
	// pagesSweptBasis)。系统在任何给定时间点处于 (gcController.heapLive,
	// pagesSwept)。
	//
	// 重要的是,线必须通过我们控制的点,而不仅仅是从 0,0 原点开始,
	// 因为这让我们可以在任何时候调整清扫节奏,同时考虑当前进度。如果我们只能
	// 调整斜率,那么如果已经取得了一些进展,会在债务上产生不连续性。
	pagesInUse         atomic.Uint64 // 在 mSpanInUse 中统计的 span 页数
	pagesSwept         atomic.Uint64 // 本周期清扫的页数
	pagesSweptBasis    atomic.Uint64 // 用作清扫比率原点的 pagesSwept 值
	sweepHeapLiveBasis uint64        // 用作清扫比率原点的 gcController.heapLive 值;带锁写入,不带锁读取
	sweepPagesPerByte  float64       // 比例清扫比率;带锁写入,不带锁读取
	// TODO(austin): pagesInUse 应该是 uintptr,但 386 编译器不能 8 字节对齐字段。

	// 页面回收状态

	// reclaimIndex 是 allArenas 中下一个要回收的页面索引。
	// 具体来说,它指的是 allArenas[i / pagesPerArena] 中的第 (i %
	// pagesPerArena) 页。
	//
	// 如果此值 >= 1<<63,页面回收器已完成扫描页面标记。
	reclaimIndex atomic.Uint64

	// reclaimCredit 是多余清扫页面的备用积分。由于页面回收器以大块工作,
	// 它可能回收比请求更多的页面。任何释放的备用页面都会进入这个积分池。
	reclaimCredit atomic.Uintptr

	// arenas 是堆 arena 映射。它指向整个可用虚拟地址空间中每个 arena 帧的元数据。
	//
	// 使用 arenaIndex 计算该数组的索引。
	//
	// 对于未由 Go 堆支持的地址空间区域,arena 映射包含 nil。
	//
	// 修改由 mheap_.lock 保护。读取可以不加锁进行;但是,当未持有锁时,
	// 给定条目可能随时从 nil 变为非 nil。(条目不会变回 nil。)
	//
	// 通常,这是一个两级映射,包括一个 L1 映射和可能的多个 L2 映射。
	// 当有大量 arena 帧时,这节省了空间。然而,在许多平台上(即使是 64 位),
	// arenaL1Bits 为 0,这使得它实际上是单级映射。在这种情况下,arenas[0]
	// 永远不会是 nil。
	arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena

	// heapArenaAlloc 是用于分配 heapArena 对象的预留空间。这仅在 32 位上使用,
	// 我们预留此空间以避免与堆本身交错。
	heapArenaAlloc linearAlloc

	// arenaHints 是一个列表,包含尝试添加更多堆 arena 的地址。最初用一组
	// 通用提示地址填充,并随着实际堆 arena 范围的边界增长。
	arenaHints *arenaHint

	// arena 是用于分配堆 arena(实际的 arena)的预留空间。这仅在 32 位上使用。
	arena linearAlloc

	// allArenas 是所有映射的 arena 的 arenaIndex。可以用它遍历地址空间。
	//
	// 访问由 mheap_.lock 保护。然而,由于这是追加的,旧的底层数组永远不会被释放,
	// 所以可以安全地获取 mheap_.lock,复制切片头,然后释放 mheap_.lock。
	allArenas []arenaIdx

	// sweepArenas 是在清扫周期开始时拍摄的 allArenas 快照。只需阻止 GC
	// (通过禁用抢占)即可安全读取。
	sweepArenas []arenaIdx

	// markArenas 是在标记周期开始时拍摄的 allArenas 快照。由于 allArenas
	// 仅追加,因此在标记期间此切片及其内容不会更改,因此可以安全读取。
	markArenas []arenaIdx

	// curArena 是堆当前正在扩展的 arena。应始终是 physPageSize 对齐的。
	curArena struct {
		base, end uintptr
	}

	_ uint32 // 确保 central 的 64 位对齐

	// 小尺寸类别的中央空闲列表。
	// padding 确保 mcentral 被 CacheLinePadSize 字节隔开,
	// 以便每个 mcentral.lock 拥有自己的缓存行。
	// central 由 spanClass 索引。    numSpanClasses=136
	central [numSpanClasses]struct {
		mcentral mcentral
        
        // 用于内存对齐
		pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
	}

	spanalloc             fixalloc // span* 的分配器
	cachealloc            fixalloc // mcache* 的分配器
	specialfinalizeralloc fixalloc // specialfinalizer* 的分配器
	specialprofilealloc   fixalloc // specialprofile* 的分配器
	specialReachableAlloc fixalloc // specialReachable 的分配器
	speciallock           mutex    // special 记录分配器的锁。
	arenaHintAlloc        fixalloc // arenaHints 的分配器

	unused *specialfinalizer // 从未设置,仅此处以强制将 specialfinalizer 类型放入 DWARF
}

heapArena

Go语言中将划分成多个arena,在amd64的架构下,每个arena的区域大小是64MB,每个arena都对应一个heapArena相当于头,用来存储arena的元数据,

heapArena记录了页到mspan的映射关系,应为在GC的时候,通过地址便宜找页很方便,但是找到其所属的mspan则不是那么方便,所以通过映射关系来维护。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type heapArena struct {
	// 
	bitmap [heapArenaBitmapBytes]byte

	
	spans [pagesPerArena]*mspan

	
	pageInUse [pagesPerArena / 8]uint8

	
	pageMarks [pagesPerArena / 8]uint8

	
	pageSpecials [pagesPerArena / 8]uint8

	
	checkmarks *checkmarksMap

	
	zeroedBase uintptr
}
  • bitmap
  • spans:将当前arena中的页面映射到mspan中

页索引

页索引是基于基数树实现的,结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
type pallocSum uint64

// packPallocSum takes a start, max, and end value and produces a pallocSum.
func packPallocSum(start, max, end uint) pallocSum {
	if max == maxPackedValue {
		return pallocSum(uint64(1 << 63))
	}
	return pallocSum((uint64(start) & (maxPackedValue - 1)) |
		((uint64(max) & (maxPackedValue - 1)) << logMaxPackedValue) |
		((uint64(end) & (maxPackedValue - 1)) << (2 * logMaxPackedValue)))
}

// start extracts the start value from a packed sum.
func (p pallocSum) start() uint {
	if uint64(p)&uint64(1<<63) != 0 {
		return maxPackedValue
	}
	return uint(uint64(p) & (maxPackedValue - 1))
}

// max extracts the max value from a packed sum.
func (p pallocSum) max() uint {
	if uint64(p)&uint64(1<<63) != 0 {
		return maxPackedValue
	}
	return uint((uint64(p) >> logMaxPackedValue) & (maxPackedValue - 1))
}

// end extracts the end value from a packed sum.
func (p pallocSum) end() uint {
	if uint64(p)&uint64(1<<63) != 0 {
		return maxPackedValue
	}
	return uint((uint64(p) >> (2 * logMaxPackedValue)) & (maxPackedValue - 1))
}

// unpack unpacks all three values from the summary.
func (p pallocSum) unpack() (uint, uint, uint) {
	if uint64(p)&uint64(1<<63) != 0 {
		return maxPackedValue, maxPackedValue, maxPackedValue
	}
	return uint(uint64(p) & (maxPackedValue - 1)),
		uint((uint64(p) >> logMaxPackedValue) & (maxPackedValue - 1)),
		uint((uint64(p) >> (2 * logMaxPackedValue)) & (maxPackedValue - 1))
}

pallocSum位64位整型,其中最高位不使用,剩下的63位分成三个部分,每个部分21位,分别代表:start,max,end。

  • mheap会基于bitMap表示内存中各页的使用情况:
    • bit位为0:表示该页是空闲的
    • bit位为1:表示该页已被mspan占用
  • 每棵基数树代表了16GB的内存空间中各个页的使用情况,用于帮助mheap能够快速的定位到指定的可以使用的空闲页
  • mheap共包含了2^14课基数树,所以能够覆盖到的最大内存空间为256T

内存分配过程

Go的对内存分配采用了tcmalloc内存分配器类似的算法。会按照一组预制的代销规格把内存划分成块,然后把不同大小的内存放到对应的空闲列表中。如:8字节,16字节,24字节,32字节,48字节等等。

image-20240615171105402

对象分类

  • 微对象:(0, 16B), 先用微型分配器奉陪,在尝试mcache、mcentral和堆分配

    分配流程:

    1. 从P的专属mcache的tiny分配器取内存(无锁)
    2. 根据所属的spanClass,从P专属的mcache缓存中区内存(无锁)
    3. 根据所属的spanClass,从对应的mcentral中取mspan填充到mcache中,然后从mspan中取内存(spanClass粒度)
    4. 根据所属的spanClass,从mheap的页分配器pageAlloc取得足够数量的空闲页组装成mspan,填充到mspan,然后从mspan中取内存(全局锁)
    5. mheap想操作系统申请内存,然后从mspan中取内存(全局锁)
  • 小对象:[16B, 32KB],依次尝试使用mcache,再尝试mcentral和堆分配

    分配流程:

    ​ 跳过第1步

  • 大对象:(32KB, +∞),直接在堆上分配

    分配流程:

    ​ 跳过1~3步

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    // ...    
    // 获取 m
    mp := acquirem()
    // 获取当前 p 对应的 mcache
    c := getMCache(mp)
    var span *mspan
    var x unsafe.Pointer
    // 根据当前对象是否包含指针,标识 gc 时是否需要展开扫描
    noscan := typ == nil || typ.ptrdata == 0
    
    // 是否是小于 32KB 的微、小对象
    if size <= maxSmallSize {
    	// 小于 16 B 且无指针,则视为微对象
        if noscan && size < maxTinySize {
          // tiny 内存块中,从 offset 往后有空闲位置
          off := c.tinyoffset
          // 如果大小为 5 ~ 8 B,size 会被调整为 8 B,此时 8 & 7 == 0,会走进此分支
          if size&7 == 0 {
                // 将 offset 补齐到 8 B 倍数的位置
                off = alignUp(off, 8)
                // 如果大小为 3 ~ 4 B,size 会被调整为 4 B,此时 4 & 3 == 0,会走进此分支  
           } else if size&3 == 0 {
           		// 将 offset 补齐到 4 B 倍数的位置
                off = alignUp(off, 4)
                // 如果大小为 1 ~ 2 B,size 会被调整为 2 B,此时 2 & 1 == 0,会走进此分支  
           } else if size&1 == 0 {
            	// 将 offset 补齐到 2 B 倍数的位置
                off = alignUp(off, 2)
           }
			// 如果当前 tiny 内存块空间还够用,则直接分配并返回
            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]    
            // 从 mCache 中获取
            v := nextFreeFast(span)        
            if v == 0 {
            // 从 mCache 中获取失败,则从 mCentral 或者 mHeap 中获取进行兜底
                v, span, shouldhelpgc = c.nextFree(tinySpanClass)
            }   
			// 分配空间      
            x = unsafe.Pointer(v)
            (*[2]uint64)(x)[0] = 0
            (*[2]uint64)(x)[1] = 0
            size = maxTinySize
        } else {
          // 根据对象大小,映射到其所属的 span 的等级(0~66)
          var sizeclass uint8
          if size <= smallSizeMax-8 {
              sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]
          } else {
              sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]
          }        
          // 对应 span 等级下,分配给每个对象的空间大小(0~32KB)
          size = uintptr(class_to_size[sizeclass])
          // 创建 spanClass 标识,其中前 7 位对应为 span 的等级(0~66),最后标识表示了这个对象 gc 时是否需要扫描
          spc := makeSpanClass(sizeclass, noscan) 
          // 获取 mcache 中的 span
          span = c.alloc[spc]  
          // 从 mcache 的 span 中尝试获取空间        
          v := nextFreeFast(span)
          if v == 0 {
          // mcache 分配空间失败,则通过 mcentral、mheap 兜底            
             v, span, shouldhelpgc = c.nextFree(spc)
          }     
          // 分配空间  
          x = unsafe.Pointer(v)
          // ...
       }      
       // 大于 32KB 的大对象      
   } else {
       // 从 mheap 中获取 0 号 span
       span = c.allocLarge(size, noscan)
       span.freeindex = 1
       span.allocCount = 1
       size = span.elemsize         
       // 分配空间   
        x = unsafe.Pointer(span.base())
   }  
   // ...
   return x
}                          


相关内容

0%