硬件到软件

go-36-001

Go语言每次申请64MB的虚拟内存,称为heapArena

最多可以有2的20次幂个

Go语言mheap内存是由许多heapArena组成

Runtime/mheap.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
}

type mheap struct {
...
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
...
}
go-36-002.png 上面图,我们可以看到,来1个人,可以吃,来多个人也可以吃,当然1个人占用1个包间,确实浪费空间,那就放到2人的餐桌吃饭,最多浪费1个位置,来3个人,就做4人餐位置,以此类推。 Go语言也使用了这样的思路,管理内存中的对象。

mspan内存管理单元。是由多个相同的内存单元组成。

runtime/mheap.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
type mspan struct {
next *mspan
prev *mspan
list *mSpanList
startAddr uintptr
npages uintptr
manualFreeList gclinkptr
nelems uintptr
allocCache uint64
allocBits *gcBits
gcmarkBits *gcBits
sweepgen uint32
divMul uint32
allocCount uint16
spanclass spanClass
state mSpanStateBox
needzero uint8
allocCountBeforeCache uint16
elemsize uintptr
limit uintptr
speciallock mutex
specials *special
}

heapArena存储的是元数据。heapArena被存储在Go堆之外,并通过mheap_.arenas索引进行访问。
type heapArena struct {
...
spans [pagesPerArena]*mspan
...
}

runtime/sizeclasses.go

go-36-003

源代码中有1-67个级别,这里只截取了1-18级

每个heapArena(64M)里不一定包含全部的内存等级小块(mspan),Go语言会根据需要进行分配的。

Go语言如何快速找到合适的mspan

go-36-004.png

runtime/mcentral.go

1
2
3
4
5
6
给定大小的可用对象的中央列表。
type mcentral struct {
spanclass spanClass
partial [2]spanSet
full [2]spanSet
}

runtime/mheap.go

1
2
3
4
5
6
7
8
9
mheap 不能被堆分配,因为它包含 mSpanLists.
type mheap struct {
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
}
numSpanClasses = _NumSizeClasses << 1
_NumSizeClasses = 68

runtime/mcache.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

它是每个线程中小对象的缓存。它包括小型对象缓存和本地分配统计信息。无需锁定,因为它是每个线程的。
mcaches 是从非 GC'd 内存中分配的,因此任何堆指针,必须特别处理。
type mcache struct {
nextSample uintptr
scanAlloc uintptr
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
stackcache [_NumStackOrders]stackfreelist
flushGen uint32
}

GMP中的P
type p struct {
...
mcache *mcache
...
}

numSpanClasses就是136,每一个mcentral都对应一个spanClass。上图的mcentral可以看到也就类似一个大市场,如果大家都来读数据,没问题,但是有人读,有人写,这自然就是并发问题,要解决就要加锁,这样的话,性能就很低,基于前面的GMP模型思想,GO语言给每个P有一个mcache,先操作这个mcache,如果写满了,那么本地的mcache再和中央mcentral的进行交换,如果中央的还是满的,那么就再开一个heapArena。

Go语言对象都多大

0 < Tiny对象<16B

16B<=Small对象<=32KB

Large对象>32KB

1-67级存放 <=32KB大小的对象(Tiny对象、Small对象),可以进行多个对象进行压缩,放在一起,节省更多的空间和开销。

Large对象就是按自身大小决定,没有固定的等级。

Go语言如何对象分配

小对象是从每 P 缓存的空闲列表中分配的。
大型对象 (> 32 kB) 直接从堆中分配。

runtime/malloc.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
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
if size <= maxSmallSize { // maxSmallSize (32k)
if noscan && size < maxTinySize {// maxTinySize 16字节
// Tiny allocator.
...
// Allocate a new maxTinySize block.
span = c.alloc[tinySpanClass]
v := nextFreeFast(span)
if v == 0 {
v, span, shouldhelpgc = c.nextFree(tinySpanClass)
}
...
}else{
var sizeclass uint8
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])
spc := makeSpanClass(sizeclass, noscan)
span = c.alloc[spc]
v := nextFreeFast(span)
if v == 0 {
v, span, shouldhelpgc = c.nextFree(spc)
}
}
}else{
...
span = c.allocLarge(size, noscan)
}
}
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
...
spc := makeSpanClass(0, noscan)
s := mheap_.alloc(npages, spc)
...
}

tiny对象

7行 tinySpanClass是2级,也就是16字节,可以通过多个tiny对象,进行压缩到这个span里

8-10行 找到一个空闲的地址,然后分配出去

small对象

14-21行 找到一个合适的span级别

22行 c是 c := getMCache(mp),然后调用alloc()

23-25行 找到合适span空间,如果找不到就去中央厂库交换调用nextFree() ->refill()

large对象

30行 创建Large对象

35行 传入0级span

Go语言的垃圾回收(GC)

那些对象是有用的

  • 栈上被引用的指针

全局引用的指针

cpu正在使用的指针

图1.
go-36-008.png

图2.
go-36-005

图3.
go-36-006

图4.
go-36-007

删除屏障
当某个对象检查完成后,再进行删除的操作,会导致扫描错误,所以一旦有删除操作,那么就把这个对象放到灰色区域中,以后再进行分析。

插入屏障

当某个对象检查完成后,再进行插入的操作,会导致扫描错误,所以一旦有插入操作,那么就把这个对象放到灰色区域中,以后再进行分析。

如何学习Go语言微服务,快速步入架构师

从0到Go语言微服务架构师-海报 从0到Go语言微服务架构师
添加微信 公众号更多内容
wechat gzh