memory
Berbeda dengan c/c++ tradisional, go adalah bahasa dengan gc, dalam kebanyakan kasus alokasi dan penghancuran memori dikelola secara otomatis oleh go, apakah memori objek harus dialokasikan di stack atau heap ditentukan oleh compiler, pada dasarnya tidak perlu pengguna berpartisipasi dalam manajemen memori, yang perlu dilakukan pengguna hanyalah menggunakan memori. Dalam go, manajemen memori heap terutama memiliki dua komponen besar, alokator memori bertanggung jawab untuk alokasi memori heap, garbage collector bertanggung jawab untuk回收 memori heap yang tidak berguna, artikel ini terutama membahas cara kerja alokator memori, alokator memori go sangat dipengaruhi oleh alokator memori TCMalloc Google.
Alokator
Dalam go terdapat dua jenis alokator memori, satu adalah alokator linear, yang lainnya adalah alokator linked list.
Alokasi Linear
Alokator linear sesuai dengan struktur runtime.linearAlloc, seperti yang ditunjukkan di bawah ini
type linearAlloc struct {
next uintptr // byte berikutnya yang tersedia
mapped uintptr // satu byte setelah akhir ruang yang dipetakan
end uintptr // akhir ruang yang dicadangkan
mapMemory bool // transisi memori dari Reserved ke Ready jika true
}Alokator ini akan meminta ruang memori kontinu dari sistem operasi di awal, next menunjuk ke alamat memori yang dapat digunakan, end menunjuk ke alamat akhir ruang memori, kira-kira dapat dipahami seperti gambar berikut.

Cara alokasi memori alokator linear sangat mudah dipahami, berdasarkan ukuran memori yang akan diminta periksa apakah ada ruang sisa yang cukup untuk menampung, jika cukup maka perbarui field next dan return alamat awal ruang sisa, kodenya seperti berikut.
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)
}Keuntungan dari cara alokasi ini adalah cepat dan sederhana, kekurangannya juga cukup jelas, yaitu tidak dapat memanfaatkan kembali memori yang telah dilepaskan, karena field next hanya akan menunjuk ke alamat memori ruang sisa, untuk ruang memori yang telah digunakan kemudian dilepaskan tidak dapat dirasakan, hal ini akan menyebabkan banyak pemborosan ruang memori, seperti yang ditunjukkan pada gambar berikut.

Jadi alokasi linear bukan cara alokasi utama dalam go, ia hanya digunakan sebagai fungsi pre-alokasi memori pada mesin 32-bit.
Alokasi Linked List
Alokator linked list sesuai dengan struktur runtime.fixalloc, memori yang dialokasikan oleh alokator linked list tidak kontinu, ada dalam bentuk linked list satu arah. Alokator linked list terdiri dari beberapa blok memori berukuran tetap, dan setiap blok memori terdiri dari beberapa slice memori berukuran tetap, setiap kali melakukan alokasi memori, akan menggunakan satu slice memori berukuran tetap.
type fixalloc struct {
size uintptr
first func(arg, p unsafe.Pointer) // dipanggil pertama kali p dikembalikan
arg unsafe.Pointer
list *mlink
chunk uintptr // gunakan uintptr alih-alih unsafe.Pointer untuk menghindari write barrier
nchunk uint32 // byte yang tersisa di chunk saat ini
nalloc uint32 // ukuran chunk baru dalam byte
inuse uintptr // byte yang sedang digunakan sekarang
stat *sysMemStat
zero bool // alokasi nol
}
type mlink struct {
_ sys.NotInHeap
next *mlink
}Field-nya tidak sederhana dan mudah dipahami seperti alokator linear, di sini saya perkenalkan sebagian yang penting
size, mengacu pada berapa banyak memori yang digunakan setiap kali alokasi memori.list, menunjuk ke node kepala slice memori yang dapat digunakan kembali, setiap slice memori berukuran ditentukan olehsize.chunk, menunjuk ke alamat空闲 di blok memori yang sedang digunakan saat ininchunk, jumlah byte yang tersedia di blok memori saat ininalloc, ukuran blok memori, tetap 16KB.inuse, total berapa byte memori yang telah digunakanzero, apakah akan mengosongkan memori saat menggunakan kembali blok memori
Alokator linked list memegang referensi ke blok memori saat ini dan slice memori yang dapat digunakan kembali, ukuran setiap blok memori tetap 16KB, nilai ini sudah diatur saat inisialisasi.
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
}Distribusi blok memori seperti yang ditunjukkan pada gambar berikut, blok memori dalam gambar diatur berdasarkan waktu pembuatan, sebenarnya alamat mereka tidak kontinu.

Ukuran memori yang dialokasikan setiap kali oleh alokator linked list juga tetap, ditentukan oleh fixalloc.size, saat alokasi akan memeriksa terlebih dahulu apakah ada blok memori yang dapat digunakan kembali, jika ada maka akan mengutamakan penggunaan blok memori yang dapat digunakan kembali, kemudian baru menggunakan blok memori saat ini, jika ruang sisa blok memori saat ini tidak cukup untuk menampung akan membuat blok memori baru, bagian logika ini sesuai dengan kode berikut.
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
}Keuntungan alokator linked list adalah dapat menggunakan kembali memori yang dilepaskan, unit dasar untuk menggunakan kembali memori adalah slice memori berukuran tetap, ukurannya ditentukan oleh fixalloc.size, saat melepaskan memori, alokator linked list akan menambahkan slice memori ini sebagai node kepala ke linked list slice memori空闲, kodenya seperti yang ditunjukkan di bawah ini
func (f *fixalloc) free(p unsafe.Pointer) {
f.inuse -= f.size
v := (*mlink)(p)
v.next = f.list
f.list = v
}Komponen Memori
Alokator memori dalam go terutama terdiri dari beberapa komponen mspan, heaparena, mcache, mcentral, mheap, mereka bekerja berlapis-lapis, mengelola seluruh memori heap go.
mspan

runtime.mspan adalah unit dasar dalam alokasi memori go, strukturnya seperti berikut
type mspan struct {
next *mspan // span berikutnya dalam list, atau nil jika tidak ada
prev *mspan // span sebelumnya dalam list, atau nil jika tidak ada
startAddr uintptr // alamat byte pertama span alias s.base()
npages uintptr // jumlah halaman dalam span
freeindex uintptr
spanclass spanClass // class ukuran dan noscan (uint8)
needzero uint8 // perlu di-nol-kan sebelum alokasi
elemsize uintptr // dihitung dari sizeclass atau dari npages
limit uintptr // akhir data dalam span
state mSpanStateBox // mSpanInUse dll; diakses secara atomik (metode get/set)
nelems uintptr // jumlah objek dalam span.
allocCache uint64
allocCount uint16 // jumlah objek yang dialokasikan
...
}mspan dan mspan dihubungkan dalam bentuk doubly linked list melalui next dan prev, alamat memori tidak kontinu. Setiap mspan mengelola memori halaman sebesar runtime.pageSize sebanyak mspan.npages, biasanya ukuran halaman adalah 8KB, dan dicatat oleh mspan.startAddr alamat awal halaman-halaman ini dan mspan.limit mencatat alamat akhir memori yang telah digunakan. Ukuran elemen yang disimpan setiap mspan elemsize adalah tetap, jadi jumlah elemen yang dapat ditampung juga tetap. Karena jumlahnya tetap, penyimpanan objek seperti array didistribusikan di mspan, rentangnya [0, nelems], sekaligus dicatat oleh freeindex indeks berikutnya yang tersedia untuk menyimpan objek. mspan total memiliki tiga status
- mSpanDead, memori telah dilepaskan
- mSpanInUse, dialokasikan ke heap
- mSpanManual, dialokasikan ke bagian untuk manajemen memori manual, seperti stack.
Yang menentukan ukuran elemen mspan adalah spanClass, spanClass sendiri adalah integer tipe uint8, tujuh bit tinggi menyimpan nilai class 0-67, bit terakhir digunakan untuk menunjukkan noscan yaitu apakah berisi pointer.
type spanClass uint8
func (sc spanClass) sizeclass() int8 {
return int8(sc >> 1)
}
func (sc spanClass) noscan() bool {
return sc&1 != 0
}Ia total memiliki 68 nilai berbeda, semua nilai disimpan dalam bentuk tabel di file runtime.sizeclasses.go, saat runtime, menggunakan spanClass melalui runtime.class_to_size dapat memperoleh ukuran objek mspan, melalui class_to_allocnpages dapat memperoleh jumlah halaman mspan.
| class | ukuran objek maksimum | ukuran span | jumlah objek | pemborosan tail | tingkat pemborosan memori maksimum | alignment minimum |
|---|---|---|---|---|---|---|
| 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 |
Tentang logika perhitungan nilai-nilai ini dapat ditemukan di fungsi printComment di runtime.mksizeclasses.go, di antaranya rumus perhitungan tingkat pemborosan memori maksimum adalah
float64((size-prevSize-1)*objects+tailWaste) / float64(spanSize)Misalnya, ketika class adalah 2, tingkat pemborosan memori maksimumnya adalah
((16-8-1)*512+0)/8192 = 0.4375Ketika nilai class adalah 0, ini adalah spanClass yang khusus digunakan untuk mengalokasikan objek besar di atas 32KB, pada dasarnya satu objek besar akan menempati satu mspan. Jadi, heap go sebenarnya terdiri dari beberapa linked list mspan dengan ukuran tetap yang berbeda.
heaparena

Sebelumnya disebutkan bahwa mspan terdiri dari beberapa halaman, tetapi mspan hanya memegang referensi alamat halaman, tidak bertanggung jawab untuk mengelola halaman-halaman ini, yang benar-benar bertanggung jawab untuk mengelola memori halaman ini adalah runtime.heaparena. Setiap heaparena mengelola beberapa halaman, ukuran heaparena ditentukan oleh runtime.heapArenaBytes, biasanya 64MB. bitmap digunakan untuk mengidentifikasi apakah alamat yang sesuai dalam halaman存放 objek, zeroedBase adalah alamat awal memori halaman yang dikelola oleh heaparena ini, dan dicatat oleh spans halaman mana yang digunakan oleh mspan mana.
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
}Tentang logika pencatatan halaman dan mspan dapat ditemukan di metode mheap.setSpans, seperti yang ditunjukkan di bawah ini
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
}
}Dalam heap go,是由一个二维的heaparena数组来管理所有的页内存,参见mheap.arenas字段。
type mheap struct {
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
}Di platform windows 64-bit, array satu dimensi adalah 1 << 6, dua dimensi adalah 1 << 16, di platform linux 64-bit, satu dimensi adalah 1, dua dimensi adalah 1 << 22. Array dua dimensi yang terdiri dari semua heaparena ini membentuk ruang memori virtual runtime go, secara keseluruhan seperti yang ditunjukkan pada gambar berikut.

Meskipun heaparena berdekatan, memori halaman yang mereka kelola tidak kontinu.
mcache
mcache sesuai dengan struktur runtime.mcache, sudah muncul dalam artikel concurrent scheduling, meskipun namanya mcache tetapi sebenarnya terikat dengan processor P. mcache adalah cache memori di setiap processor P,其中包括mspan linked list数组alloc, ukuran array tetap136, tepat dua kali jumlah spanClass,还有微对象缓存tiny,其中tiny指向微对象内存的起始地址,tinyoffset则是空闲内存相对于起始地址的偏移量,tinyAllocs表示分配了多少个微对象。关于栈缓存stackcached,可以前往栈内存分配进行了解。
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
}Saat inisialisasi, linked list di alloc di mcache hanya berisi node kepala kosong runtime.emptymspan, yaitu mspan tanpa memori yang tersedia.
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
}Hanya ketika perlu melakukan alokasi memori, akan meminta mspan baru dari mcentral untuk mengganti span kosong asli, bagian pekerjaan ini diselesaikan oleh metode mcache.refill, satu-satunya入口 pemanggilan adalah fungsi runtime.mallocgc, berikut adalah kode yang disederhanakan.
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
}Keuntungan menggunakan mcache adalah alokasi memori tidak memerlukan lock global, tetapi ketika memorinya tidak cukup perlu mengakses mcentral, saat ini masih perlu加锁。
mcentral
runtime.mcentral mengelola semua mspan yang存放 objek kecil di heap, saat mcache meminta memori juga dialokasikan oleh mcentral.
type mcentral struct {
_ sys.NotInHeap
spanclass spanClass
partial [2]spanSet
full [2]spanSet
}Field mcentral sangat sedikit, spanClass menunjukkan jenis mspan yang disimpan, partial dan full adalah dua spanSet, yang pertama存放mspan dengan memori空闲, yang kedua存放mspan tanpa memori空闲。mcentral dikelola langsung oleh heap mheap, saat runtime total ada 136 mcentral.
type mheap struct {
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
}mcentral terutama bertanggung jawab untuk dua pekerjaan, ketika memori cukup mengalokasikan mspan yang tersedia ke mcache, ketika memori tidak cukup meminta alokasi mspan baru dari mheap. Pekerjaan mengalokasikan mspan ke mcache diselesaikan oleh metode mcentral.cacheSpan. Pertama akan mencari mspan yang tersedia di set yang telah dibersihkan di daftar空闲。
// Try partial swept spans first.
sg := mheap_.sweepgen
if s = c.partialSwept(sg).pop(); s != nil {
goto havespan
}Jika tidak ditemukan, cari mspan yang tersedia di set yang belum dibersihkan di daftar空闲
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
}
}Jika masih tidak ditemukan, cari di set yang belum dibersihkan di daftar non-idle
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)
}
}Jika pada akhirnya masih tidak ditemukan, maka akan oleh metode mcentral.grow meminta alokasi mspan baru dari mheap.
s = c.grow()
if s == nil {
return nil
}Dalam situasi normal, bagaimanapun akan return mspan yang tersedia.
havespan:
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
// Init alloc bits cache.
s.refillAllocCache(whichByte)
s.allocCache >>= s.freeindex % 64
return sUntuk proses meminta mspan dari mheap, sebenarnya memanggil metode mheap.alloc, metode ini akan return mspan baru.
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
}Setelah menginisialisasinya dengan baik dapat dialokasikan ke mcache untuk digunakan.
mheap

runtimme.mheap adalah pengelola memori heap bahasa go, saat runtime ada sebagai variabel global runtime.mheap_.
var mheap_ mheapIa mengelola semua mspan yang dibuat, semua mcentral, dan semua heaparena, serta banyak alokator lainnya, strukturnya yang disederhanakan seperti berikut
type mheap struct {
_ sys.NotInHeap
lock mutex
allspans []*mspan // semua span di luar sana
pagesInUse atomic.Uintptr // halaman span dalam statistik mSpanInUse
pagesSwept atomic.Uint64 // halaman yang disapu siklus ini
pagesSweptBasis atomic.Uint64 // pagesSwept untuk digunakan sebagai asal rasio sweep
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 // struktur data alokasi halaman
spanalloc fixalloc // alokator untuk span*
cachealloc fixalloc // alokator untuk mcache*
specialfinalizeralloc fixalloc // alokator untuk specialfinalizer*
specialprofilealloc fixalloc // alokator untuk specialprofile*
specialReachableAlloc fixalloc // alokator untuk specialReachable
specialPinCounterAlloc fixalloc // alokator untuk specialPinCounter
arenaHintAlloc fixalloc // alokator untuk arenaHints
}Untuk mheap, saat runtime terutama ada empat pekerjaan yang harus dilakukan
- Inisialisasi heap
- Alokasi
mspan - Pelepasan
mspan - Ekspansi heap
Di bawah ini akan menceritakan keempat hal ini sesuai urutan.
Inisialisasi
Periode inisialisasi heap terletak pada tahap bootstrap program, sekaligus tahap inisialisasi scheduler, urutan pemanggilan adalah
schedinit() -> mallocinit() -> mheap_.init()Pada periode inisialisasi, ia terutama bertanggung jawab untuk melaksanakan pekerjaan inisialisasi masing-masing alokator
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)
}Di antaranya termasuk alokator mheap.spanalloc yang bertanggung jawab untuk mengalokasikan mspan dan alokator mheap.pages yang bertanggung jawab untuk alokasi halaman, serta inisialisasi semua mcentral.
Alokasi
Di mheap, alokasi mspan semuanya diselesaikan oleh metode mheap.allocSpan
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan)Jika memori yang diminta untuk dialokasikan cukup kecil, yaitu memenuhi npages < pageCachePages/4, maka akan mencoba不加锁 di cache mspan di P lokal untuk mendapatkan mspan yang tersedia, jika cache P kosong, akan menginisialisasi terlebih dahulu
// If the cache is empty, refill it.
if c.empty() {
lock(&h.lock)
*c = h.pages.allocToCache()
unlock(&h.lock)
}Kemudian mendapatkan dari cache P, diselesaikan oleh metode mheap.tryAllocMSpan.
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
}
}
}Kode untuk mendapatkan mspan dari cache P seperti berikut, ia akan mencoba mendapatkan mspan terakhir di 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
}Jika memori yang diminta cukup besar, akan mengalokasikan memori di heap, proses ini perlu持有锁
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)Pertama akan menggunakan pageAlloc.alloc untuk mengalokasikan memori halaman yang cukup, jika memori heap tidak cukup akan oleh mheap.grow untuk ekspansi. Setelah alokasi memori halaman selesai, akan oleh alokator linked list mheap.spanalloc mengalokasikan 64 mspan ke cache lokal P, 64 tepat setengah dari panjang array cache, kemudian return mspan yang tersedia dari cache 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
}Menurut dua situasi di atas, pada akhirnya dapat mendapatkan mspan yang tersedia, terakhir setelah menginisialisasi mspan dapat return
HaveSpan:
h.initSpan(s, typ, spanclass, base, npages)
return sPelepasan
Karena mspan dialokasikan oleh alokator linked list, secara alami saat melepaskan memori juga olehnya untuk melepaskan.
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)
}Pertama akan menandai melalui alokator halaman mheap.pages bahwa memori halaman yang ditentukan telah dilepaskan, kemudian mengatur status mspan menjadi mSpanDead, terakhir oleh alokator mheap.spanalloc melepaskan 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))
}Jika cache P belum penuh, akan menempatkannya di cache lokal P untuk terus digunakan, jika tidak akan dilepaskan kembali ke memori heap.
Ekspansi
Ruang memori halaman yang dikelola oleh heaparena tidak semuanya sudah diminta di awal, hanya akan dialokasikan ketika perlu menggunakan memori. Yang bertanggung jawab untuk ekspansi memori heap adalah metode mheap.grow, berikut adalah kode yang disederhanakan.
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)
}
...
}Ia pertama akan menghitung memori yang diperlukan berdasarkan npage dan melakukan alignment, kemudian判断 apakah heaparena saat ini memiliki memori yang cukup, jika tidak cukup akan oleh mheap.sysAlloc meminta lebih banyak memori untuk heaparena saat ini atau mengalokasikan heaparena baru.
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
}
}
...
}Pertama akan mencoba menggunakan alokator linear mheap.arena untuk meminta sepotong memori di ruang memori yang telah dialokasikan di awal, jika gagal akan melakukan ekspansi berdasarkan hintList, tipe hintList adalah runtime.arenaHint, ia khusus mencatat informasi alamat terkait ekspansi 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))
}Setelah memori diminta, kemudian perbarui ke array dua dimensi 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))
}Terakhir再由页分配器将这片内存标记为就绪状态。
// Update the page allocator's structures to make this
// space ready for allocation.
h.pages.grow(v, nBase-v)
totalGrowth += nBase - vAlokasi Objek
Go saat mengalokasikan memori untuk objek, berdasarkan ukuran dibagi menjadi tiga jenis yang berbeda:
- Objek mikro - tiny, kurang dari 16B
- Objek kecil - small, kurang dari 32KB
- Objek besar - large, lebih dari 32KB
Berdasarkan tiga jenis yang berbeda, saat alokasi memori akan menjalankan logika yang berbeda. Fungsi yang bertanggung jawab untuk mengalokasikan memori objek adalah runtime.mallocgc, signature fungsinya seperti berikut
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.PointerIa hanya memiliki tiga parameter, ukuran memori, tipe, dan nilai boolean用于表示是否需要清空内存。它是所有 go 对象内存分配的入口函数,平时在使用new函数创建指针时同样也会走入该函数,当内存分配成功后,它返回的指针就是该对象的地址。在 mspan 部分中提到过,每一个 mspan 都拥有一个spanClass,spanClass决定了mspan 的固定大小,并且 go 将对象从 [0, 32KB] 的范围分成了 68 种不同的大小,所以 go 内存由若干个不同的大小固定的mspan 链表组成。在分配对象内存时,只需按照对象大小计算出对应的spanClass,然后再根据spanClass 找到对应的mspan 链表,最后再从链表中寻找可用的mspan,这种分级的做法能较为有效的解决内存碎片的问题。

Objek Mikro
Semua objek mikro non-pointer yang kurang dari 16B akan oleh alokator mikro di P dialokasikan ke sepotong memori kontinu yang sama, di runitme.mcache, field tiny mencatat alamat dasar memori ini.
type mcache struct {
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
}Ukuran objek mikro ditentukan oleh konstanta runtime.maxTinySize, semuanya 16B,用于存储微对象的内存块同样也是这个大小,一般来说这里存储的对象都是一些小字符串,负责分配微对象的部分代码如下所示。
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 = maxTinySizeJika blok memori mikro saat ini masih memiliki ruang yang cukup untuk menampung, langsung gunakan blok memori saat ini, yaitu off+size <= maxTinySize. Jika tidak cukup, akan pertama mencoba mencari ruang yang tersedia dari cache span mcache, jika juga tidak bisa akan meminta mspan dari mcentral, bagaimanapun pada akhirnya akan mendapatkan alamat yang tersedia, terakhir再用新的微对象内存块替换掉旧的。
Objek Kecil
Sebagian besar objek dalam runtime bahasa go adalah objek kecil dalam rentang [16B, 32KB], proses alokasi objek kecil paling merepotkan, tetapi kodenya paling sedikit, bagian kode yang bertanggung jawab untuk alokasi objek kecil seperti berikut.
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)
}Pertama akan menghitung berdasarkan ukuran objek应该使用哪一类的spanClass, kemudian oleh runtime.nextFreeFast berdasarkan spanClass mencoba mendapatkan ruang memori yang tersedia dari cache mspan yang sesuai di mcache.
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
}Fungsi mspan.allocCache adalah untuk mencatat apakah ruang memori telah digunakan oleh objek, dan ia membagi memori satu per satu berdasarkan jumlah objek bukan berdasarkan ukuran ruang, ini相当于把mspan 看作一个对象数组,如下图所示。

allocCache adalah angka 64-bit, setiap bit sesuai dengan sepotong ruang memori, jika某一位为 0 表示有对象使用,如果是 1 的话表示这片内存是空闲的。sys.TrailingZeros64(s.allocCache) 的目的就是计算尾随零的数量,如果结果是 64 的话则表明没有空闲的内存可以使用,如果有的话再计算得到空闲内存的偏移量加上mspan 的基地址然后返回。
Ketika mcache tidak memiliki ruang yang cukup, akan pergi ke mcentral untuk meminta, bagian pekerjaan ini diselesaikan oleh metode mcache.nextFree
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
}Di antaranya mcache.refill akan bertanggung jawab untuk meminta mspan yang tersedia dari mcentral.
func (c *mcache) refill(spc spanClass) {
...
s = mheap_.central[spc].mcentral.cacheSpan()
...
}Dan metode mcentral.cacheSpan akan melakukan ekspansi oleh mcentral.grow ketika memori tidak cukup, ekspansi kemudian akan meminta mspan baru dari mheap.
func (c *mcentral) grow() *mspan {
...
s := mheap_.alloc(npages, c.spanclass)
...
return s
}Jadi pada akhirnya terlihat, alokasi memori objek kecil adalah berjalan tingkat demi tingkat ke bawah, pertama mcache, kemudian mcentral, terakhir mheap. Biaya alokasi mcache paling rendah, karena ia adalah cache lokal P, alokasi memori tidak perlu持有锁,mcentral berikutnya, langsung meminta memori dari mheap biaya paling tinggi, karena metode mheap.alloc akan bersaing lock global seluruh heap.
Objek Besar
Alokasi objek besar paling sederhana, jika ukuran objek melebihi 32KB, akan langsung meminta alokasi mspan baru dari mheap untuk menampung, bagian kode yang bertanggung jawab untuk alokasi objek besar seperti berikut.
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)
}
}Di antaranya mcache.allocLarge bertanggung jawab untuk meminta ruang memori objek besar dari mheap
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
...
spc := makeSpanClass(0, noscan)
s := mheap_.alloc(npages, spc)
...
return s
}Dari kode dapat dilihat bahwa spanClass yang digunakan objek besar adalah nilai 0, pada dasarnya satu objek besar menempati satu mspan.
Lainnya
Statistik Memori
Runtime go mengekspos fungsi ReadMemStats kepada pengguna, dapat digunakan untuk统计 situasi memori 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()
}Tetapi biaya penggunaannya sangat besar, dari kode dapat dilihat sebelum menganalisis situasi memori perlu STW, dan durasi STW mungkin beberapa milidetik hingga ratusan milidetik, umumnya hanya digunakan saat debugging dan troubleshooting. Struktur runtime.MemStats mencatat informasi terkait memori heap, memori stack, dan 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
Alokator memori jelas digunakan untuk mengalokasikan memori heap, tetapi heap dibagi menjadi dua bagian, satu bagian adalah memori heap yang dibutuhkan oleh runtime go sendiri, bagian lainnya adalah memori heap yang dibuka untuk pengguna. Jadi di beberapa struktur dapat melihat field embedded seperti ini
_ sys.NotInHeapMenunjukkan bahwa memori tipe ini tidak akan dialokasikan di heap pengguna, field embedded ini terutama umum dalam komponen alokasi memori, seperti struktur yang表示用户堆的结构体runtime.mheap
type mheap struct {
_ sys.NotInHeap
}Fungsi sebenarnya dari sys.NotInHeap adalah untuk menghindari memory barrier guna meningkatkan efisiensi runtime, sedangkan heap pengguna perlu menjalankan GC sehingga memerlukan memory barrier.
