memory
Khác với c/c++ truyền thống, go là một ngôn ngữ có gc, trong hầu hết các trường hợp việc phân phối và giải phóng bộ nhớ do go tự động quản lý, bộ nhớ của một đối tượng nên được phân phối trên stack hay heap do compiler quyết định, về cơ bản không cần người dùng tham gia vào quản lý bộ nhớ, việc người dùng cần làm chỉ là sử dụng bộ nhớ. Trong go, quản lý bộ nhớ heap chủ yếu có hai component lớn, bộ phân phối bộ nhớ负责 phân phối bộ nhớ heap, bộ thu gom rác负责 thu hồi giải phóng bộ nhớ vô dụng, bài viết này chủ yếu讲的是 cách làm việc của bộ phân phối bộ nhớ, bộ phân phối bộ nhớ của go chịu ảnh hưởng rất lớn từ bộ phân phối bộ nhớ TCMalloc của google.
Bộ phân phối
Trong go có hai loại bộ phân phối bộ nhớ, một loại là bộ phân phối tuyến tính, loại khác là bộ phân phối chuỗi.
Phân phối tuyến tính
Bộ phân phối tuyến tính tương ứng với cấu trúc runtime.linearAlloc, như dưới đây
type linearAlloc struct {
next uintptr // next free byte
mapped uintptr // one byte past end of mapped space
end uintptr // end of reserved space
mapMemory bool // transition memory from Reserved to Ready if true
}Bộ phân phối này sẽ xin cấp từ hệ điều hành một không gian bộ nhớ liên tục, next trỏ đến địa chỉ bộ nhớ có thể sử dụng, end trỏ đến địa chỉ cuối của không gian bộ nhớ, đại khái có thể hiểu như hình dưới.

Cách phân phối bộ nhớ của bộ phân phối tuyến tính rất dễ hiểu, dựa vào kích thước bộ nhớ xin cấp để kiểm tra xem có đủ không gian dư để chứa không, nếu đủ thì cập nhật trường next và trả về địa chỉ bắt đầu của không gian dư, code như sau.
func (l *linearAlloc) alloc(size, align uintptr, sysStat *sysMemStat) unsafe.Pointer {
p := alignUp(l.next, align)
if p+size > l.end {
return nil
}
l.next = p + size
return unsafe.Pointer(p)
}Ưu điểm của cách phân phối này là nhanh và đơn giản, nhược điểm cũng khá rõ ràng, là không thể tái sử dụng bộ nhớ đã giải phóng, vì trường next chỉ trỏ đến địa chỉ bộ nhớ không gian dư, đối với không gian bộ nhớ đã sử dụng rồi giải phóng trước đó thì không thể cảm nhận được, làm như vậy sẽ gây lãng phí không gian bộ nhớ rất lớn, như hình dưới.

Nên phân phối tuyến tính không phải là cách phân phối chủ yếu trong go, nó chỉ được sử dụng như chức năng phân phối bộ nhớ trước trên máy 32 bit.
Phân phối chuỗi
Bộ phân phối chuỗi tương ứng với cấu trúc runtime.fixalloc, bộ nhớ mà bộ phân phối chuỗi phân phối không liên tục, tồn tại dưới dạng linked list đơn. Bộ phân phối chuỗi do若干 khối bộ nhớ cố định kích thước组成, mà mỗi khối bộ nhớ do若干片 bộ nhớ cố định kích thước组成, mỗi lần phân phối bộ nhớ, sẽ sử dụng một片 bộ nhớ cố định kích thước.
type fixalloc struct {
size uintptr
first func(arg, p unsafe.Pointer) // called first time p is returned
arg unsafe.Pointer
list *mlink
chunk uintptr // use uintptr instead of unsafe.Pointer to avoid write barriers
nchunk uint32 // bytes remaining in current chunk
nalloc uint32 // size of new chunks in bytes
inuse uintptr // in-use bytes now
stat *sysMemStat
zero bool // zero allocations
}
type mlink struct {
_ sys.NotInHeap
next *mlink
}Trường của nó không đơn giản dễ hiểu như bộ phân phối tuyến tính, ở đây giới thiệu đơn giản một số trường quan trọng
size, chỉ mỗi lần phân phối bộ nhớ sử dụng bao nhiêu bộ nhớ.list, trỏ đến node đầu của khối bộ nhớ có thể tái sử dụng, mỗi片 không gian bộ nhớ kích thước dosizequyết định.chunk, trỏ đến địa chỉ空闲 của khối bộ nhớ đang sử dụng hiện tạinchunk, số byte khả dụng dư của khối bộ nhớ hiện tạinalloc, kích thước khối bộ nhớ, cố định 16KB.inuse,总共 đã sử dụng bao nhiêu byte bộ nhớzero, khi tái sử dụng khối bộ nhớ, có cần xóa bộ nhớ không
Bộ phân phối chuỗi giữ reference của khối bộ nhớ hiện tại và khối bộ nhớ có thể tái sử dụng, kích thước mỗi khối bộ nhớ đều cố định 16KB, giá trị này được thiết lập khi khởi tạo.
const _FixAllocChunk = 16 << 10
func (f *fixalloc) init(size uintptr, first func(arg, p unsafe.Pointer), arg unsafe.Pointer, stat *sysMemStat) {
if size > _FixAllocChunk {
throw("runtime: fixalloc size too large")
}
if min := unsafe.Sizeof(mlink{}); size < min {
size = min
}
f.size = size
f.first = first
f.arg = arg
f.list = nil
f.chunk = 0
f.nchunk = 0
f.nalloc = uint32(_FixAllocChunk / size * size)
f.inuse = 0
f.stat = stat
f.zero = true
}Phân bố khối bộ nhớ như hình dưới, trong hình khối bộ nhớ được sắp xếp theo thứ tự thời gian tạo, thực tế địa chỉ của chúng không liên tục.

Kích thước bộ nhớ mà bộ phân phối chuỗi mỗi lần phân phối cũng cố định, do fixalloc.size quyết định, khi phân phối sẽ trước tiên kiểm tra xem có khối bộ nhớ có thể tái sử dụng không, nếu có thì ưu tiên sử dụng khối bộ nhớ tái sử dụng, rồi mới đi sử dụng khối bộ nhớ hiện tại, nếu không gian dư của khối bộ nhớ hiện tại không đủ để chứa thì sẽ tạo một khối bộ nhớ mới, phần logic này tương ứng với code dưới.
func (f *fixalloc) alloc() unsafe.Pointer {
if f.size == 0 {
print("runtime: use of FixAlloc_Alloc before FixAlloc_Init\n")
throw("runtime: internal error")
}
if f.list != nil {
v := unsafe.Pointer(f.list)
f.list = f.list.next
f.inuse += f.size
if f.zero {
memclrNoHeapPointers(v, f.size)
}
return v
}
if uintptr(f.nchunk) < f.size {
f.chunk = uintptr(persistentalloc(uintptr(f.nalloc), 0, f.stat))
f.nchunk = f.nalloc
}
v := unsafe.Pointer(f.chunk)
if f.first != nil {
f.first(f.arg, v)
}
f.chunk = f.chunk + f.size
f.nchunk -= uint32(f.size)
f.inuse += f.size
return v
}Ưu điểm của bộ phân phối chuỗi chính là nó có thể tái sử dụng bộ nhớ đã giải phóng, đơn vị cơ bản của việc tái sử dụng bộ nhớ là một片 bộ nhớ cố định kích thước, kích thước của nó do fixalloc.size quyết định, khi giải phóng bộ nhớ, bộ phân phối chuỗi sẽ lấy片 bộ nhớ này làm node đầu thêm vào linked list bộ nhớ空闲, code như dưới
func (f *fixalloc) free(p unsafe.Pointer) {
f.inuse -= f.size
v := (*mlink)(p)
v.next = f.list
f.list = v
}Component bộ nhớ
Bộ phân phối bộ nhớ trong go chủ yếu do các component msapn, heaparena, mcache, mcentral, mheap cấu thành, chúng giữa tầng tầng tác dụng, quản lý toàn bộ bộ nhớ heap của go.
mspan

runtime.mspan là đơn vị cơ bản trong phân phối bộ nhớ go, cấu trúc của nó như sau
type mspan struct {
next *mspan // next span in list, or nil if none
prev *mspan // previous span in list, or nil if none
startAddr uintptr // address of first byte of span aka s.base()
npages uintptr // number of pages in span
freeindex uintptr
spanclass spanClass // size class and noscan (uint8)
needzero uint8 // needs to be zeroed before allocation
elemsize uintptr // computed from sizeclass or from npages
limit uintptr // end of data in span
state mSpanStateBox // mSpanInUse etc; accessed atomically (get/set methods)
nelems uintptr // number of object in the span.
allocCache uint64
allocCount uint16 // number of allocated objects
...
}Giữa mspan và mspan liên kết dưới dạng doubly linked list thông qua next và prev, địa chỉ bộ nhớ không liên tục. Mỗi msapn quản lý mspan.npages trang bộ nhớ kích thước runtime.pageSize, thông thường kích thước trang là 8KB, và do mspan.startAddr ghi lại địa chỉ bắt đầu của các trang này và mspan.limit ghi lại địa chỉ cuối của bộ nhớ đã sử dụng. Kích thước elemsize của phần tử mà mỗi mspan lưu trữ là cố định, nên số lượng phần tử có thể chứa cũng cố định. Vì số lượng cố định, việc lưu trữ đối tượng giống như array phân bố trong mspan, phạm vi là [0, nelems], đồng thời do freeindex ghi lại index có thể dùng để lưu trữ đối tượng tiếp theo. mspan总共 có ba trạng thái
- mSpanDead, bộ nhớ đã được giải phóng
- mSpanInUse, được phân phối lên heap
- mSpanManual, được phân phối vào phần dùng để quản lý bộ nhớ thủ công, ví dụ như stack.
Cái quyết định kích thước phần tử của mspan là spanClass, spanClass tự nó là một số nguyên kiểu uint8, 7 bit cao lưu trữ giá trị class biểu thị 0-67, bit cuối cùng dùng để biểu thị noscan tức có chứa pointer không.
type spanClass uint8
func (sc spanClass) sizeclass() int8 {
return int8(sc >> 1)
}
func (sc spanClass) noscan() bool {
return sc&1 != 0
}Nó总共 có 68 giá trị khác nhau, tất cả giá trị đều dưới dạng table lưu trữ trong file runtime.sizeclasses.go, trong runtime, sử dụng spanClass thông qua runtime.class_to_size có thể nhận được kích thước đối tượng của mspan, thông qua class_to_allocnpages có thể nhận được số trang của mspan.
| class | Kích thước đối tượng tối đa | Kích thước span | Số lượng đối tượng | Lãng phí đuôi | Tỷ lệ lãng phí bộ nhớ tối đa | Căn chỉnh tối thiểu |
|---|---|---|---|---|---|---|
| 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 |
Về logic tính toán của các giá trị này có thể tìm thấy trong hàm printComment của runtime.mksizeclasses.go, trong đó công thức tính tỷ lệ lãng phí bộ nhớ tối đa là
float64((size-prevSize-1)*objects+tailWaste) / float64(spanSize)Ví dụ, khi class là 2, tỷ lệ lãng phí bộ nhớ tối đa của nó là
((16-8-1)*512+0)/8192 = 0.4375Khi giá trị class là 0, chính là spanClass chuyên dùng để phân phối đối tượng lớn hơn 32KB, về cơ bản một đối tượng lớn sẽ chiếm một mspan. Nên, bộ nhớ heap của go thực tế là do若干 mspan链表 cố định kích thước khác nhau组成。
heaparena

前面 đã đề cập mspan là do若干 trang组成, nhưng mspan chỉ giữ reference địa chỉ của trang, không负责 quản lý các trang này, cái thực sự负责 quản lý các trang bộ nhớ này là runtime.heaparena. Mỗi heaparena quản lý若干 trang, kích thước của heaparena do runtime.heapArenaBytes quyết định, thông thường là 64MB. bitmap dùng để nhận biết xem địa chỉ trong trang có lưu trữ đối tượng không, zeroedBase chính là địa chỉ bắt đầu của trang bộ nhớ mà heaparena này quản lý, và do spans ghi lại mỗi trang do mspan nào sử dụng.
type heapArena struct {
_ sys.NotInHeap
bitmap [heapArenaBitmapWords]uintptr
noMorePtrs [heapArenaBitmapWords / 8]uint8
spans [pagesPerArena]*mspan
pageInUse [pagesPerArena / 8]uint8
pageMarks [pagesPerArena / 8]uint8
pageSpecials [pagesPerArena / 8]uint8
checkmarks *checkmarksMap
zeroedBase uintptr
}Về logic ghi lại trang và mspan có thể tìm thấy trong phương thức mheap.setSpans, như dưới
func (h *mheap) setSpans(base, npage uintptr, s *mspan) {
p := base / pageSize
ai := arenaIndex(base)
ha := h.arenas[ai.l1()][ai.l2()]
for n := uintptr(0); n < npage; n++ {
i := (p + n) % pagesPerArena
if i == 0 {
ai = arenaIndex(base + n*pageSize)
ha = h.arenas[ai.l1()][ai.l2()]
}
ha.spans[i] = s
}
}Trong heap của go, là do một mảng二维 heaparena để quản lý tất cả trang bộ nhớ, xem trường mheap.arenas.
type mheap struct {
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
}Trên nền tảng windows 64 bit, mảng một chiều là 1 << 6, hai chiều là 1 << 16, trên nền tảng linux 64 bit, một chiều thì là 1, hai chiều chính là 1 << 22. Mảng二维 do tất cả heaparena组成 này cấu thành không gian bộ nhớ ảo của runtime go, nhìn tổng thể như hình dưới.

Mặc dù giữa các heaparena liền kề, nhưng trang bộ nhớ mà chúng quản lý giữa không liên tục.
mcache
mcache tương ứng với cấu trúc runtime.mcache, trong bài viết về concurrent scheduling đã từng xuất hiện, mặc dù tên của nó gọi là mcache nhưng thực tế nó được bind với processor P. mcache là bộ nhớ cache bộ nhớ của mỗi processor P, trong đó chứa linked list array alloc của mspan, kích thước array cố định là 136, vừa đúng là hai lần số lượng spanClass, còn có bộ nhớ cache đối tượng nhỏ tiny, trong đó tiny trỏ đến địa chỉ bắt đầu của bộ nhớ đối tượng nhỏ, tinyoffset là offset của bộ nhớ空闲相对于 địa chỉ bắt đầu, tinyAllocs biểu thị đã phân phối bao nhiêu đối tượng nhỏ. Về bộ nhớ cache stack stackcached, có thể đến Phân phối bộ nhớ stack để tìm hiểu.
type mcache struct {
_ sys.NotInHeap
nextSample uintptr // trigger heap sample after allocating this many bytes
scanAlloc uintptr // bytes of scannable heap allocated
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
alloc [numSpanClasses]*mspan
stackcache [_NumStackOrders]stackfreelist
flushGen atomic.Uint32
}Khi vừa khởi tạo, linked list trong alloc của mcache đều chỉ chứa một node đầu rỗng runtime.emptymspan,也就是 không có mspan khả dụng.
func allocmcache() *mcache {
var c *mcache
systemstack(func() {
lock(&mheap_.lock)
c = (*mcache)(mheap_.cachealloc.alloc())
c.flushGen.Store(mheap_.sweepgen)
unlock(&mheap_.lock)
})
for i := range c.alloc {
c.alloc[i] = &emptymspan
}
c.nextSample = nextSample()
return c
}Chỉ khi cần phân phối bộ nhớ, mới sẽ xin cấp từ mcentral một mspan mới để thay thế span rỗng ban đầu, phần công việc này do phương thức mcache.refill hoàn thành,入口 gọi duy nhất của nó là hàm runtime.mallocgc, dưới đây là code đã đơn giản hóa.
func (c *mcache) refill(spc spanClass) {
// Return the current cached span to the central lists.
s := c.alloc[spc]
// Get a new cached span from the central lists.
s = mheap_.central[spc].mcentral.cacheSpan()
if s == nil {
throw("out of memory")
}
c.scanAlloc = 0
c.alloc[spc] = s
}Lợi ích của việc sử dụng mcache là khi phân phối bộ nhớ không cần khóa toàn cục, nhưng khi bộ nhớ của nó không đủ cần truy cập mcentral, lúc này vẫn cần加锁。
mcentral
runtime.mcentral quản lý tất cả mspan lưu trữ đối tượng nhỏ trong heap, khi mcache xin cấp bộ nhớ cũng do mcentral进行 phân phối.
type mcentral struct {
_ sys.NotInHeap
spanclass spanClass
partial [2]spanSet
full [2]spanSet
}Trường của mcentral rất ít, spanClass biểu thị loại mspan được lưu trữ, partial và full là hai spanSet, cái trước lưu trữ mspan có bộ nhớ空闲, cái sau lưu trữ mspan không có bộ nhớ空闲。mcentral do heap mheap trực tiếp quản lý, trong runtime总共 có 136 mcentral.
type mheap struct {
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
}mcentral chủ yếu负责 hai công việc, khi bộ nhớ đủ thì phân phối mspan khả dụng cho mcache, khi bộ nhớ không đủ thì xin cấp phân phối từ mheap một mspan mới. Công việc phân phối mspan cho mcache do phương thức mcentral.cacheSpan hoàn thành. Trước tiên sẽ tìm kiếm mspan khả dụng trong tập hợp đã quét của danh sách空闲。
// Try partial swept spans first.
sg := mheap_.sweepgen
if s = c.partialSwept(sg).pop(); s != nil {
goto havespan
}Nếu không tìm thấy, thì tìm kiếm mspan khả dụng trong tập hợp chưa quét của danh sách空闲
for ; spanBudget >= 0; spanBudget-- {
s = c.partialUnswept(sg).pop()
if s == nil {
break
}
if s, ok := sl.tryAcquire(s); ok {
s.sweep(true)
sweep.active.end(sl)
goto havespan
}
}Nếu vẫn không tìm thấy, thì đi tìm kiếm trong tập hợp chưa quét của danh sách không空闲
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()
if freeIndex != s.nelems {
s.freeindex = freeIndex
sweep.active.end(sl)
goto havespan
}
c.fullSwept(sg).push(s.mspan)
}
}Nếu cuối cùng vẫn không tìm thấy, thì sẽ do phương thức mcentral.grow xin cấp phân phối từ mheap một mspan mới.
s = c.grow()
if s == nil {
return nil
}Trong tình huống bình thường, bất kể thế nào đều sẽ trả về một mspan khả dụng.
havespan:
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
// Init alloc bits cache.
s.refillAllocCache(whichByte)
s.allocCache >>= s.freeindex % 64
return sĐối với quá trình xin cấp mspan từ mheap, thực tế là gọi phương thức mheap.alloc, phương thức này sẽ trả về một mspan mới.
func (c *mcentral) grow() *mspan {
npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
size := uintptr(class_to_size[c.spanclass.sizeclass()])
s := mheap_.alloc(npages, c.spanclass)
if s == nil {
return nil
}
n := s.divideByElemSize(npages << _PageShift)
s.limit = s.base() + size*n
s.initHeapBits(false)
return s
}Sau khi khởi tạo xong nó có thể phân phối cho mcache sử dụng.
mheap

runtimme.mheap là người quản lý bộ nhớ heap của ngôn ngữ go, trong runtime nó tồn tại như biến toàn cục runtime.mheap_.
var mheap_ mheapNó quản lý tất cả mspan được tạo, tất cả mcentral, và tất cả heaparena, còn có rất nhiều bộ phân phối khác, cấu trúc đã đơn giản hóa của nó như sau
type mheap struct {
_ sys.NotInHeap
lock mutex
allspans []*mspan // all spans out there
pagesInUse atomic.Uintptr // pages of spans in stats mSpanInUse
pagesSwept atomic.Uint64 // pages swept this cycle
pagesSweptBasis atomic.Uint64 // pagesSwept to use as the origin of the sweep ratio
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
allArenas []arenaIdx
sweepArenas []arenaIdx
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
pages pageAlloc // page allocation data structure
spanalloc fixalloc // allocator for span*
cachealloc fixalloc // allocator for mcache*
specialfinalizeralloc fixalloc // allocator for specialfinalizer*
specialprofilealloc fixalloc // allocator for specialprofile*
specialReachableAlloc fixalloc // allocator for specialReachable
specialPinCounterAlloc fixalloc // allocator for specialPinCounter
arenaHintAlloc fixalloc // allocator for arenaHints
}Đối với mheap mà nói, trong runtime chủ yếu có bốn công việc cần làm
- Khởi tạo heap
- Phân phối
mspan - Giải phóng
mspan - Mở rộng heap
Dưới đây theo thứ tự来讲 bốn việc này.
Khởi tạo
Thời kỳ khởi tạo heap nằm ở giai đoạn bootstrap của chương trình, đồng thời cũng là giai đoạn khởi tạo của bộ điều phối, thứ tự gọi là
schedinit() -> mallocinit() -> mheap_.init()Trong thời kỳ khởi tạo, nó chủ yếu负责 thực hiện công việc khởi tạo của các bộ phân phối
func (h *mheap) init() {
h.spanalloc.init(unsafe.Sizeof(mspan{}), recordspan, unsafe.Pointer(h), &memstats.mspan_sys)
h.cachealloc.init(unsafe.Sizeof(mcache{}), nil, nil, &memstats.mcache_sys)
h.specialfinalizeralloc.init(unsafe.Sizeof(specialfinalizer{}), nil, nil, &memstats.other_sys)
h.specialprofilealloc.init(unsafe.Sizeof(specialprofile{}), nil, nil, &memstats.other_sys)
h.specialReachableAlloc.init(unsafe.Sizeof(specialReachable{}), nil, nil, &memstats.other_sys)
h.specialPinCounterAlloc.init(unsafe.Sizeof(specialPinCounter{}), nil, nil, &memstats.other_sys)
h.arenaHintAlloc.init(unsafe.Sizeof(arenaHint{}), nil, nil, &memstats.other_sys)
h.spanalloc.zero = false
for i := range h.central {
h.central[i].mcentral.init(spanClass(i))
}
h.pages.init(&h.lock, &memstats.gcMiscSys, false)
}Trong đó bao gồm bộ phân phối负责 phân phối mspan mheap.spanalloc và bộ phân phối负责 phân phối trang mheap.pages, cũng như khởi tạo của tất cả mcentral.
Phân phối
Trong mheap, phân phối mspan đều do phương thức mheap.allocSpan hoàn thành
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan)Nếu bộ nhớ xin cấp phân phối đủ nhỏ, tức thỏa mãn npages < pageCachePages/4, thì sẽ thử không加锁 trong bộ nhớ cache mspan của P本地 để lấy một mspan khả dụng, nếu bộ nhớ cache của P là rỗng thì còn sẽ khởi tạo trước
// If the cache is empty, refill it.
if c.empty() {
lock(&h.lock)
*c = h.pages.allocToCache()
unlock(&h.lock)
}Rồi mới lấy từ bộ nhớ cache của P, do phương thức mheap.tryAllocMSpan hoàn thành.
pp := gp.m.p.ptr()
if !needPhysPageAlign && pp != nil && npages < pageCachePages/4 {
c := &pp.pcache
base, scav = c.alloc(npages)
if base != 0 {
s = h.tryAllocMSpan()
if s != nil {
goto HaveSpan
}
}
}Code lấy mspan từ bộ nhớ cache của P như sau, nó sẽ thử lấy mspan cuối cùng trong bộ nhớ cache.
func (h *mheap) tryAllocMSpan() *mspan {
pp := getg().m.p.ptr()
// If we don't have a p or the cache is empty, we can't do
// anything here.
if pp == nil || pp.mspancache.len == 0 {
return nil
}
// Pull off the last entry in the cache.
s := pp.mspancache.buf[pp.mspancache.len-1]
pp.mspancache.len--
return s
}Nếu bộ nhớ xin cấp phân phối khá lớn thì sẽ phân phối trên heap, quá trình này cần giữ khóa
lock(&h.lock)
if base == 0 {
// Try to acquire a base address.
base, scav = h.pages.alloc(npages)
if base == 0 {
var ok bool
growth, ok = h.grow(npages)
if !ok {
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 {
// We failed to get an mspan earlier, so grab
// one now that we have the heap lock.
s = h.allocMSpanLocked()
}
unlock(&h.lock)Trước tiên sẽ sử dụng pageAlloc.alloc để phân phối đủ trang bộ nhớ cho nó, nếu bộ nhớ heap không đủ thì sẽ do mheap.grow进行 mở rộng. Sau khi phân phối trang bộ nhớ xong, sẽ do bộ phân phối chuỗi mheap.spanalloc phân phối 64 mspan vào bộ nhớ cache của P本地, 64 vừa đúng là một nửa độ dài array bộ nhớ cache, rồi mới trả về một mspan khả dụng từ bộ nhớ cache của P.
func (h *mheap) allocMSpanLocked() *mspan {
assertLockHeld(&h.lock)
pp := getg().m.p.ptr()
if pp == nil {
// We don't have a p so just do the normal thing.
return (*mspan)(h.spanalloc.alloc())
}
// Refill the cache if necessary.
if pp.mspancache.len == 0 {
const refillCount = len(pp.mspancache.buf) / 2
for i := 0; i < refillCount; i++ {
pp.mspancache.buf[i] = (*mspan)(h.spanalloc.alloc())
}
pp.mspancache.len = refillCount
}
// Pull off the last entry in the cache.
s := pp.mspancache.buf[pp.mspancache.len-1]
pp.mspancache.len--
return s
}Căn cứ vào hai tình huống trên, cuối cùng đều có thể nhận được một mspan khả dụng, cuối cùng sau khi khởi tạo mspan xong thì có thể trả về
HaveSpan:
h.initSpan(s, typ, spanclass, base, npages)
return sGiải phóng
既然 mspan là do bộ phân phối chuỗi phân phối, tự nhiên khi giải phóng bộ nhớ cũng do nó进行 giải phóng.
func (h *mheap) freeSpanLocked(s *mspan, typ spanAllocType) {
assertLockHeld(&h.lock)
// Mark the space as free.
h.pages.free(s.base(), s.npages)
s.state.set(mSpanDead)
h.freeMSpanLocked(s)
}Trước tiên sẽ thông qua bộ phân phối trang mheap.pages đánh dấu chỉ định trang bộ nhớ được giải phóng, rồi đặt trạng thái của mspan thành mSpanDead, cuối cùng do bộ phân phối mheap.spanalloc giải phóng mspan.
func (h *mheap) freeMSpanLocked(s *mspan) {
assertLockHeld(&h.lock)
pp := getg().m.p.ptr()
// First try to free the mspan directly to the cache.
if pp != nil && pp.mspancache.len < len(pp.mspancache.buf) {
pp.mspancache.buf[pp.mspancache.len] = s
pp.mspancache.len++
return
}
// Failing that (or if we don't have a p), just free it to
// the heap.
h.spanalloc.free(unsafe.Pointer(s))
}Nếu bộ nhớ cache của P chưa đầy, thì sẽ đặt nó vào bộ nhớ cache của P本地 để tiếp tục sử dụng, nếu không thì nó sẽ được giải phóng trở lại bộ nhớ heap.
Mở rộng
Không gian bộ nhớ trang mà heaparena quản lý không phải đã được xin cấp hết trong thời kỳ đầu, chỉ khi cần dùng đến bộ nhớ mới đi phân phối. Cái负责 mở rộng bộ nhớ heap là phương thức mheap.grow, dưới đây là code đã đơn giản hóa.
func (h *mheap) grow(npage uintptr) (uintptr, bool) {
assertLockHeld(&h.lock)
ask := alignUp(npage, pallocChunkPages) * pageSize
totalGrowth := uintptr(0)
end := h.curArena.base + ask
nBase := alignUp(end, physPageSize)
if nBase > h.curArena.end || end < h.curArena.base {
av, asize := h.sysAlloc(ask, &h.arenaHints, true)
if uintptr(av) == h.curArena.end {
h.curArena.end = uintptr(av) + asize
} else {
// Switch to the new space.
h.curArena.base = uintptr(av)
h.curArena.end = uintptr(av) + asize
}
nBase = alignUp(h.curArena.base+ask, physPageSize)
}
...
}Nó trước tiên sẽ tính toán dựa vào npage bộ nhớ cần thiết và căn chỉnh, rồi判断 xem heaparena hiện tại có đủ bộ nhớ không, nếu không đủ thì sẽ do mheap.sysAlloc cho heaparena hiện tại xin cấp thêm bộ nhớ hoặc phân phối một heaparena mới.
func (h *mheap) sysAlloc(n uintptr, hintList **arenaHint, register bool) (v unsafe.Pointer, size uintptr) {
n = alignUp(n, heapArenaBytes)
if hintList == &h.arenaHints {
v = h.arena.alloc(n, heapArenaBytes, &gcController.heapReleased)
if v != nil {
size = n
goto mapped
}
}
...
}Trước tiên sẽ thử sử dụng bộ phân phối tuyến tính mheap.arena xin cấp một khối bộ nhớ trong không gian bộ nhớ đã phân phối trước, nếu thất bại thì dựa vào hintList进行 mở rộng, kiểu của hintList là runtime.arenaHint, nó chuyên ghi lại thông tin địa chỉ liên quan đến mở rộng heaparena.
for *hintList != nil {
hint := *hintList
p := hint.addr
v = sysReserve(unsafe.Pointer(p), n)
if p == uintptr(v) {
hint.addr = p
size = n
break
}
if v != nil {
sysFreeOS(v, n)
}
*hintList = hint.next
h.arenaHintAlloc.free(unsafe.Pointer(hint))
}Sau khi xin cấp bộ nhớ xong, lại cập nhật nó vào array二维 arenas
for ri := arenaIndex(uintptr(v)); ri <= arenaIndex(uintptr(v)+size-1); ri++ {
l2 := h.arenas[ri.l1()]
var r *heapArena
r = (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), goarch.PtrSize, &memstats.gcMiscSys))
atomic.StorepNoWB(unsafe.Pointer(&l2[ri.l2()]), unsafe.Pointer(r))
}Cuối cùng lại do bộ phân phối trang đánh dấu这片 bộ nhớ là trạng thái sẵn sàng.
// Update the page allocator's structures to make this
// space ready for allocation.
h.pages.grow(v, nBase-v)
totalGrowth += nBase - vPhân phối đối tượng
Khi go phân phối bộ nhớ cho đối tượng, dựa vào kích thước chia thành ba loại khác nhau:
- Đối tượng nhỏ - tiny, nhỏ hơn 16B
- Đối tượng nhỏ - small, nhỏ hơn 32KB
- Đối tượng lớn - large, lớn hơn 32KB
Dựa vào ba loại khác nhau, khi phân phối bộ nhớ sẽ thực hiện logic khác nhau. Hàm负责 phân phối bộ nhớ cho đối tượng là runtime.mallocgc, signature của hàm như sau
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.PointerNó chỉ có ba tham số, kích thước bộ nhớ, kiểu, và một boolean dùng để biểu thị có cần xóa bộ nhớ không. Nó là入口 hàm phân phối bộ nhớ của tất cả đối tượng go, bình thường khi sử dụng hàm new tạo pointer cũng sẽ đi vào hàm này, khi phân phối bộ nhớ thành công, pointer mà nó trả về chính là địa chỉ của đối tượng đó. Trong phần mspan đã đề cập, mỗi mspan đều sở hữu một spanClass, spanClass quyết định kích thước cố định của mspan, và go chia đối tượng từ [0, 32KB] phạm vi thành 68 loại kích thước khác nhau, nên bộ nhớ go do若干 mspan链表 cố định kích thước khác nhau组成。Khi phân phối bộ nhớ đối tượng, chỉ cần dựa vào kích thước đối tượng tính toán ra spanClass tương ứng, rồi dựa vào spanClass tìm mspan链表 tương ứng, cuối cùng lại từ链表中寻找 mspan khả dụng, cách làm phân cấp này có thể giải quyết vấn đề memory fragmentation khá hiệu quả.

Đối tượng nhỏ
Tất cả đối tượng nhỏ không phải pointer nhỏ hơn 16B sẽ do bộ phân phối nhỏ trong P được phân phối vào cùng一片 bộ nhớ liên tục, trong runitme.mcache, do trường tiny ghi lại địa chỉ cơ sở của这片 bộ nhớ.
type mcache struct {
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
}Kích thước của đối tượng nhỏ do hằng số runtime.maxTinySize quyết định, đều là 16B, khối bộ nhớ dùng để lưu trữ đối tượng nhỏ cũng là kích thước này, thông thường ở đây lưu trữ đối tượng đều là một số string nhỏ, phần code负责 phân phối đối tượng nhỏ như sau.
if size <= maxSmallSize {
if noscan && size < maxTinySize {
off := c.tinyoffset
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
}
// Allocate a new maxTinySize block.
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 (size < c.tinyoffset || c.tiny == 0) {
c.tiny = uintptr(x)
c.tinyoffset = size
}
size = maxTinySizeNếu khối bộ nhớ nhỏ hiện tại vẫn có đủ không gian để chứa, thì trực tiếp sử dụng khối bộ nhớ hiện tại, tức off+size <= maxTinySize. Nếu không đủ thì sẽ trước tiên thử từ bộ nhớ cache span của mcache tìm kiếm không gian khả dụng, nếu cũng không được thì sẽ xin cấp từ mcentral một mspan, bất kể thế nào cuối cùng đều sẽ nhận được một địa chỉ khả dụng, cuối cùng lại dùng khối bộ nhớ đối tượng nhỏ mới thay thế khối cũ.
Đối tượng nhỏ
Phần lớn đối tượng trong ngôn ngữ go runtime đều là đối tượng nhỏ nằm trong phạm vi [16B, 32KB] này, quá trình phân phối đối tượng nhỏ phiền phức nhất, nhưng code lại là ít nhất, phần code负责 phân phối đối tượng nhỏ như sau.
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)
}
x = unsafe.Pointer(v)
if needzero && span.needzero != 0 {
memclrNoHeapPointers(x, size)
}Trước tiên sẽ dựa vào kích thước đối tượng tính toán xem nên sử dụng spanClass loại nào, rồi do runtime.nextFreeFast dựa vào spanClass thử đi vào bộ nhớ cache mspan tương ứng trong mcache lấy không gian bộ nhớ khả dụng.
func nextFreeFast(s *mspan) gclinkptr {
theBit := sys.TrailingZeros64(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
}Tác dụng của mspan.allocCache là ghi lại xem không gian bộ nhớ có đối tượng sử dụng không, và nó dựa vào số lượng đối tượng để划分 bộ nhớ từng cái một chứ không phải dựa vào kích thước không gian để划分, điều này tương đương với xem mspan như một array đối tượng, như hình dưới.

allocCache là một số 64 bit, mỗi bit tương ứng với一片 không gian bộ nhớ, nếu bit nào đó là 0 biểu thị có đối tượng sử dụng, nếu là 1 thì biểu thị这片 bộ nhớ là空闲。Mục đích của sys.TrailingZeros64(s.allocCache) là tính số lượng zero đuôi, nếu kết quả là 64 thì biểu thị không có bộ nhớ空闲 có thể sử dụng, nếu có thì lại tính toán得到 offset của bộ nhớ空闲 cộng với địa chỉ cơ sở của mspan rồi trả về.
Khi mcache không có đủ không gian, thì sẽ đi xin cấp từ mcentral, phần công việc này do phương thức mcache.nextFree hoàn thành
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
s = c.alloc[spc]
shouldhelpgc = false
freeIndex := s.nextFreeIndex()
if freeIndex == s.nelems {
c.refill(spc)
shouldhelpgc = true
s = c.alloc[spc]
freeIndex = s.nextFreeIndex()
}
v = gclinkptr(freeIndex*s.elemsize + s.base())
s.allocCount++
return
}Trong đó mcache.refill sẽ负责 xin cấp từ mcentral một mspan khả dụng.
func (c *mcache) refill(spc spanClass) {
...
s = mheap_.central[spc].mcentral.cacheSpan()
...
}Mà phương thức mcentral.cacheSpan sẽ do mcentral.grow进行 mở rộng khi bộ nhớ không đủ, mở rộng thì lại sẽ đi xin cấp từ mheap mspan mới.
func (c *mcentral) grow() *mspan {
...
s := mheap_.alloc(npages, c.spanclass)
...
return s
}Nên cuối cùng xem ra, phân phối bộ nhớ đối tượng nhỏ là từng cấp từng cấp đi xuống, trước là mcache, rồi là mcentral, cuối cùng là mheap. Chi phí phân phối của mcache thấp nhất, vì nó là bộ nhớ cache của P本地, phân phối bộ nhớ không cần giữ khóa, mcentral其次, trực tiếp xin cấp bộ nhớ từ mheap chi phí cao nhất, vì phương thức mheap.alloc sẽ cạnh tranh khóa toàn cục của cả heap.
Đối tượng lớn
Phân phối đối tượng lớn đơn giản nhất, nếu kích thước đối tượng vượt quá 32KB, thì sẽ trực tiếp xin cấp từ mheap phân phối một mspan mới để chứa, phần code负责 phân phối đối tượng lớn như sau.
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)
}
}Trong đó mcache.allocLarge负责 xin cấp từ mheap không gian bộ nhớ của đối tượng lớn
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
...
spc := makeSpanClass(0, noscan)
s := mheap_.alloc(npages, spc)
...
return s
}Từ code có thể thấy là spanClass mà đối tượng lớn sử dụng giá trị là 0, đối tượng lớn về cơ bản là một đối tượng chiếm một mpan.
Khác
Thống kê bộ nhớ
Go runtime expose cho người dùng một hàm ReadMemStats, có thể dùng để thống kê tình hình bộ nhớ của runtime.
func ReadMemStats(m *MemStats) {
_ = m.Alloc // nil check test before we switch stacks, see issue 61158
stopTheWorld(stwReadMemStats)
systemstack(func() {
readmemstats_m(m)
})
startTheWorld()
}Nhưng cái giá phải trả cho việc sử dụng nó rất lớn, từ code có thể thấy trước khi phân tích tình hình bộ nhớ cần STW, mà thời gian STW có thể là vài mili giây đến vài trăm mili giây không đều, thông thường chỉ sử dụng khi debug và troubleshooting. Cấu trúc runtime.MemStats ghi lại thông tin liên quan đến bộ nhớ heap, bộ nhớ stack, và GC
type MemStats struct {
// 总体统计
Alloc uint64
TotalAlloc uint64
Sys uint64
Lookups uint64
Mallocs uint64
Frees uint64
// 堆内存统计
HeapAlloc uint64
HeapSys uint64
HeapIdle uint64
HeapInuse uint64
HeapReleased uint64
HeapObjects uint64
// 栈内存统计
StackInuse uint64
StackSys uint64
// 内存组件统计
MSpanInuse uint64
MSpanSys uint64
MCacheInuse uint64
MCacheSys uint64
BuckHashSys uint64
// gc 相关的统计
GCSys uint64
OtherSys uint64
NextGC uint64
LastGC uint64
PauseTotalNs uint64
PauseNs [256]uint64
PauseEnd [256]uint64
NumGC uint32
NumForcedGC uint32
GCCPUFraction float64
EnableGC bool
DebugGC bool
BySize [61]struct {
Size uint32
Mallocs uint64
Frees uint64
}
}NotInHeap
Bộ phân phối bộ nhớ rõ ràng dùng để phân phối bộ nhớ heap, nhưng heap lại được chia thành hai phần, một phần là bộ nhớ heap mà go runtime tự thân cần, phần khác là bộ nhớ heap mở cho người dùng sử dụng. Nên trong một số cấu trúc có thể thấy trường embedded như vậy
_ sys.NotInHeapBiểu thị bộ nhớ của kiểu này sẽ không được phân phối trên user heap, loại trường embedded này trong component phân phối bộ nhớ尤为 phổ biến, ví dụ như cấu trúc thể hiện user heap runtime.mheap
type mheap struct {
_ sys.NotInHeap
}Tác dụng thực sự của sys.NotInHeap là để tránh memory barrier nhằm nâng cao hiệu suất runtime, mà user heap cần chạy GC nên cần memory barrier.
