memory
A differenza del tradizionale c/c++, Go è un linguaggio con GC, e nella maggior parte dei casi l'allocazione e la distruzione della memoria sono gestite automaticamente da Go. Se la memoria di un oggetto deve essere allocata sullo stack o sull'heap è deciso dal compilatore, e fondamentalmente non richiede l'intervento dell'utente nella gestione della memoria. L'utente deve solo utilizzare la memoria. In Go, la gestione della memoria heap ha due componenti principali: l'allocatore di memoria è responsabile dell'allocazione della memoria heap, e il garbage collector è responsabile del recupero e del rilascio della memoria heap inutile. Questo articolo tratta principalmente del funzionamento dell'allocatore di memoria di Go, che è largamente influenzato dall'allocatore di memoria TCMalloc di Google.
Allocatore
In Go ci sono due tipi di allocatori di memoria: l'allocatore lineare e l'allocatore a catena.
Allocazione Lineare
L'allocatore lineare corrisponde alla struttura runtime.linearAlloc, come mostrato di seguito:
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
}Questo allocatore richiede preventivamente al sistema operativo uno spazio di memoria continuo, next punta all'indirizzo di memoria utilizzabile, end punta all'indirizzo di fine dello spazio di memoria, che può essere approssimativamente compreso come nella figura seguente:

Il modo di allocazione della memoria dell'allocatore lineare è molto facile da capire: in base alla dimensione della memoria da richiedere, controlla se c'è abbastanza spazio rimanente per contenerla. Se c'è abbastanza spazio, aggiorna il campo next e restituisce l'indirizzo iniziale dello spazio rimanente, come nel codice seguente:
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)
}Questo metodo di allocazione ha il vantaggio di essere veloce e semplice, ma lo svantaggio è altrettanto evidente: non può riutilizzare la memoria già rilasciata, poiché il campo next punta solo all'indirizzo di memoria dello spazio rimanente, e non può percepire lo spazio di memoria già utilizzato e poi rilasciato. Questo causa un grande spreco di spazio di memoria, come mostrato nella figura seguente:

Quindi l'allocazione lineare non è il metodo principale di allocazione in Go, viene utilizzata solo come funzione di pre-allocazione della memoria su macchine a 32 bit.
Allocazione a Catena
L'allocatore a catena corrisponde alla struttura runtime.fixalloc. La memoria allocata dall'allocatore a catena non è continua, ma esiste sotto forma di una lista concatenata singola. L'allocatore a catena è composto da diversi blocchi di memoria di dimensione fissa, e ogni blocco di memoria è composto da diversi frammenti di memoria di dimensione fissa. Ogni volta che viene eseguita un'allocazione di memoria, viene utilizzato un frammento di memoria di dimensione fissa.
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
}I suoi campi non sono semplici e facili da capire come quelli dell'allocatore lineare, qui vengono introdotti brevemente solo i campi importanti:
size: indica quanta memoria viene utilizzata ogni volta che viene allocata memoria.list: punta al nodo iniziale della lista dei frammenti di memoria riutilizzabili, la dimensione di ogni frammento di memoria è determinata dasize.chunk: punta all'indirizzo idle del blocco di memoria attualmente in uso.nchunk: numero di byte disponibili rimanenti del blocco di memoria corrente.nalloc: dimensione del blocco di memoria, fissa a 16KB.inuse: totale di byte di memoria già utilizzati.zero: indica se cancellare la memoria quando si riutilizza un blocco di memoria.
L'allocatore a catena mantiene un riferimento al blocco di memoria corrente e ai frammenti di memoria riutilizzabili. La dimensione di ogni blocco di memoria è fissa a 16KB, questo valore viene impostato durante l'inizializzazione.
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
}La distribuzione dei blocchi di memoria è mostrata nella figura seguente. I blocchi di memoria nella figura sono disposti in ordine di tempo di creazione, ma in realtà i loro indirizzi non sono continui.

La dimensione della memoria allocata ogni volta dall'allocatore a catena è anche fissa, determinata da fixalloc.size. Durante l'allocazione, controlla prima se ci sono blocchi di memoria riutilizzabili. Se ci sono, utilizza prima i blocchi di memoria riutilizzabili, poi utilizza il blocco di memoria corrente. Se lo spazio rimanente del blocco di memoria corrente non è sufficiente a contenere la memoria richiesta, crea un nuovo blocco di memoria. Questa parte della logica corrisponde al codice seguente:
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
}Il vantaggio dell'allocatore a catena è proprio che può riutilizzare la memoria rilasciata. L'unità di base per il riutilizzo della memoria è un frammento di memoria di dimensione fissa, la cui dimensione è determinata da fixalloc.size. Quando si rilascia memoria, l'allocatore a catena aggiunge il frammento di memoria come nodo iniziale alla lista dei frammenti di memoria idle, come nel codice seguente:
func (f *fixalloc) free(p unsafe.Pointer) {
f.inuse -= f.size
v := (*mlink)(p)
v.next = f.list
f.list = v
}Componenti di Memoria
L'allocatore di memoria in Go è composto principalmente dai seguenti componenti: mspan, heaparena, mcache, mcentral, mheap. Questi componenti lavorano a livelli, gestendo l'intera memoria heap di Go.
mspan

runtime.mspan è l'unità di base nell'allocazione di memoria di Go, la sua struttura è la seguente:
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
...
}mspan e mspan sono collegati tramite next e prev sotto forma di una lista concatenata doppia, e gli indirizzi di memoria non sono continui. Ogni mspan gestisce mspan.npages pagine di memoria di dimensione runtime.pageSize. Di solito la dimensione della pagina è 8KB, e mspan.startAddr registra l'indirizzo iniziale di queste pagine e mspan.limit registra l'indirizzo finale della memoria utilizzata. La dimensione degli elementi memorizzati in ogni mspan, elemsize, è fissa, quindi anche il numero di elementi che può contenere è fisso. Poiché il numero è fisso, gli oggetti sono distribuiti in mspan come in un array, nell'intervallo [0, nelems], e freeindex registra l'indice successivo disponibile per memorizzare oggetti. mspan ha tre stati:
- mSpanDead: la memoria è stata rilasciata
- mSpanInUse: allocata sull'heap
- mSpanManual: allocata per la parte di memoria gestita manualmente, come lo stack.
Ciò che determina la dimensione degli elementi di mspan è spanClass. spanClass è di per sé un intero di tipo uint8, i primi sette bit memorizzano il valore class da 0 a 67, e l'ultimo bit è utilizzato per indicare noscan, cioè se contiene puntatori.
type spanClass uint8
func (sc spanClass) sizeclass() int8 {
return int8(sc >> 1)
}
func (sc spanClass) noscan() bool {
return sc&1 != 0
}Ha un totale di 68 valori diversi, e tutti questi valori sono memorizzati sotto forma di tabella nel file runtime.sizeclasses.go. A runtime, utilizzando spanClass si può ottenere la dimensione dell'oggetto di mspan tramite runtime.class_to_size, e il numero di pagine di mspan tramite class_to_allocnpages.
| class | dimensione massima oggetto | dimensione span | numero oggetti | spreco coda | tasso massimo spreco memoria | allineamento minimo |
|---|---|---|---|---|---|---|
| 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 |
La logica di calcolo di questi valori può essere trovata nella funzione printComment in runtime.mksizeclasses.go. La formula per il tasso massimo di spreco di memoria è:
float64((size-prevSize-1)*objects+tailWaste) / float64(spanSize)Ad esempio, quando class è 2, il tasso massimo di spreco di memoria è:
((16-8-1)*512+0)/8192 = 0.4375Quando il valore di class è 0, è il spanClass utilizzato specificamente per allocare oggetti grandi superiori a 32KB. Fondamentalmente un oggetto grande occupa un mspan. Quindi, la memoria heap di Go è effettivamente composta da diverse liste concatenate mspan di dimensioni fisse.
heaparena

Come menzionato in precedenza, mspan è composto da diverse pagine, ma mspan detiene solo un riferimento all'indirizzo delle pagine e non è responsabile della gestione di queste pagine. Ciò che gestisce realmente queste pagine di memoria è runtime.heaparena. Ogni heaparena gestisce diverse pagine, e la dimensione di heaparena è determinata da runtime.heapArenaBytes, di solito 64MB. bitmap è utilizzato per identificare se gli indirizzi nelle pagine contengono oggetti, zeroedBase è l'indirizzo iniziale delle pagine di memoria gestite da questo heaparena, e spans registra quale mspan utilizza ogni pagina.
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
}La logica relativa alle pagine e ai record mspan può essere trovata nel metodo mheap.setSpans, come mostrato di seguito:
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
}
}Nella heap di Go, è un array bidimensionale di heaparena che gestisce tutte le pagine di memoria, vedi il campo mheap.arenas.
type mheap struct {
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
}Su piattaforme Windows a 64 bit, la prima dimensione dell'array è 1 << 6, la seconda è 1 << 16. Su piattaforme Linux a 64 bit, la prima dimensione è 1, la seconda è 1 << 22. Questo array bidimensionale composto da tutti i heaparena costituisce lo spazio di memoria virtuale del runtime di Go, che nel complesso è mostrato nella figura seguente:

Sebbene i heaparena siano adiacenti, le pagine di memoria che gestiscono non sono continue tra loro.
mcache
mcache corrisponde alla struttura runtime.mcache, che è già apparsa nell'articolo sulla schedulazione concorrente. Sebbene il suo nome sia mcache, è effettivamente legato al processore P. mcache è la cache di memoria di ogni processore P, che include l'array di liste concatenate mspan alloc, la cui dimensione è fissa a 136,刚好 è il doppio del numero di spanClass. Include anche la cache di micro-oggetti tiny, dove tiny punta all'indirizzo iniziale della memoria dei micro-oggetti, tinyoffset è l'offset della memoria idle rispetto all'indirizzo iniziale, e tinyAllocs indica quanti micro-oggetti sono stati allocati. Per la cache stack stackcache, si può fare riferimento a Allocazione Memoria Stack.
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
}All'inizializzazione, le liste concatenate in alloc di mcache contengono solo un nodo iniziale vuoto runtime.emptymspan, cioè un mspan senza memoria disponibile.
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
}Solo quando è necessario allocare memoria, viene richiesto un nuovo mspan a mcentral per sostituire lo span vuoto originale. Questo lavoro è completato dal metodo mcache.refill, il cui unico punto di ingresso è la funzione runtime.mallocgc. Di seguito il codice semplificato:
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
}Il vantaggio di utilizzare mcache è che l'allocazione di memoria non richiede un lock globale, ma quando la memoria è insufficiente, è necessario accedere a mcentral, che richiede ancora un lock.
mcentral
runtime.mcentral gestisce tutti i mspan nella heap che contengono piccoli oggetti, e quando mcache richiede memoria, è mcentral che la alloca.
type mcentral struct {
_ sys.NotInHeap
spanclass spanClass
partial [2]spanSet
full [2]spanSet
}I campi di mcentral sono pochi: spanClass indica il tipo di mspan memorizzato, partial e full sono due spanSet, il primo memorizza mspan con memoria idle, il secondo memorizza mspan senza memoria idle. mcentral è gestito direttamente dalla heap mheap, e a runtime ci sono un totale di 136 mcentral.
type mheap struct {
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
}mcentral è principalmente responsabile di due compiti: allocare mspan disponibili a mcache quando c'è memoria sufficiente, e richiedere a mheap di allocare un nuovo mspan quando la memoria è insufficiente. Il lavoro di allocazione di mspan a mcache è completato dal metodo mcentral.cacheSpan. Prima cerca mspan disponibili nell'insieme spazzato della lista idle.
// Try partial swept spans first.
sg := mheap_.sweepgen
if s = c.partialSwept(sg).pop(); s != nil {
goto havespan
}Se non ne trova, cerca mspan disponibili nell'insieme non spazzato della lista idle.
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
}
}Se ancora non ne trova, cerca nell'insieme non spazzato della lista 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)
}
}Se alla fine non ne trova, il metodo mcentral.grow richiede a mheap di allocare un nuovo mspan.
s = c.grow()
if s == nil {
return nil
}In condizioni normali, restituisce sempre un mspan disponibile.
havespan:
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
// Init alloc bits cache.
s.refillAllocCache(whichByte)
s.allocCache >>= s.freeindex % 64
return sPer il processo di richiesta di mspan a mheap, viene effettivamente chiamato il metodo mheap.alloc, che restituisce un nuovo mspan.
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
}Dopo averlo inizializzato correttamente, può essere allocato a mcache per l'uso.
mheap

runtime.mheap è il gestore della memoria heap del linguaggio Go. A runtime esiste come variabile globale runtime.mheap_.
var mheap_ mheapGestisce tutti i mspan creati, tutti i mcentral, e tutti i heaparena, oltre a molti altri allocatori di vario tipo. La sua struttura semplificata è la seguente:
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
}Per mheap, a runtime ci sono principalmente quattro compiti da svolgere:
- Inizializzare la heap
- Allocare
mspan - Rilasciare
mspan - Espandere la heap
Di seguito vengono illustrati questi quattro compiti in ordine.
Inizializzazione
L'inizializzazione della heap si trova nella fase di bootstrap del programma, che è anche la fase di inizializzazione dello scheduler. La sequenza di chiamata è:
schedinit() -> mallocinit() -> mheap_.init()Durante l'inizializzazione, è principalmente responsabile dell'esecuzione del lavoro di inizializzazione di ciascun allocatore:
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)
}Questo include l'allocatore mheap.spanalloc responsabile dell'allocazione di mspan, l'allocatore mheap.pages responsabile dell'allocazione delle pagine, e l'inizializzazione di tutti i mcentral.
Allocazione
In mheap, l'allocazione di mspan è completata dal metodo mheap.allocSpan:
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan)Se la memoria da allocare è sufficientemente piccola, cioè soddisfa npages < pageCachePages/4, prova a ottenere un mspan disponibile dalla cache mspan del P locale senza lock. Se la cache del P è vuota, la inizializza prima:
// If the cache is empty, refill it.
if c.empty() {
lock(&h.lock)
*c = h.pages.allocToCache()
unlock(&h.lock)
}Poi ottiene dalla cache del P, completato dal metodo 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
}
}
}Il codice per ottenere mspan dalla cache del P è il seguente, prova a ottenere l'ultimo mspan nella 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
}Se la memoria richiesta è relativamente grande, viene allocata sulla heap, e durante questo processo è necessario mantenere il lock:
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)Prima utilizza pageAlloc.alloc per allocare sufficiente memoria di pagina. Se la memoria heap non è sufficiente, mheap.grow esegue l'espansione. Dopo il completamento dell'allocazione della memoria di pagina, l'allocazione a catena mheap.spanalloc alloca 64 mspan alla cache locale del P, dove 64 è esattamente la metà della lunghezza dell'array della cache, poi restituisce un mspan disponibile dalla cache del 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
}In base alle due situazioni sopra, alla fine si può ottenere un mspan disponibile. Dopo aver inizializzato correttamente mspan, può essere restituito:
HaveSpan:
h.initSpan(s, typ, spanclass, base, npages)
return sRilascio
Poiché mspan è allocato dall'allocatore a catena, naturalmente anche il rilascio della memoria viene eseguito da esso.
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)
}Prima utilizza l'allocatore di pagine mheap.pages per marcare le pagine di memoria specificate come rilasciate, poi imposta lo stato di mspan a mSpanDead, infine l'allocatore mheap.spanalloc rilascia 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))
}Se la cache del P non è piena, viene inserita nella cache locale del P per continuare l'uso, altrimenti viene rilasciata alla memoria heap.
Espansione
Lo spazio di memoria delle pagine gestito da heaparena non è stato completamente richiesto nella fase iniziale, ma viene allocato solo quando è necessario utilizzare la memoria. Ciò che è responsabile dell'espansione della memoria heap è il metodo mheap.grow. Di seguito il codice semplificato:
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)
}
...
}Prima calcola la memoria richiesta in base a npage e la allinea, poi determina se l'attuale heaparena ha memoria sufficiente. Se non c'è abbastanza memoria, mheap.sysAlloc richiede più memoria per l'attuale heaparena o alloca un nuovo heaparena.
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
}
}
...
}Prima prova a utilizzare l'allocatore lineare mheap.arena per richiedere un blocco di memoria nello spazio di memoria pre-allocato. Se fallisce, esegue l'espansione in base a hintList. hintList è di tipo runtime.arenaHint, che registra specificamente le informazioni di indirizzo relative all'espansione di 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))
}Dopo aver richiesto la memoria, la aggiorna nell'array bidimensionale 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))
}Infine, l'allocatore di pagine marca questo spazio di memoria come stato pronto.
// Update the page allocator's structures to make this
// space ready for allocation.
h.pages.grow(v, nBase-v)
totalGrowth += nBase - vAllocazione di Oggetti
Quando Go alloca memoria per gli oggetti, li divide in tre tipi diversi in base alla dimensione:
- Micro oggetti - tiny, inferiori a 16B
- Piccoli oggetti - small, inferiori a 32KB
- Grandi oggetti - large, superiori a 32KB
In base a questi tre tipi diversi, viene eseguita logica diversa durante l'allocazione della memoria. La funzione responsabile dell'allocazione della memoria per gli oggetti è runtime.mallocgc, la cui firma è la seguente:
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.PointerHa solo tre parametri: dimensione della memoria, tipo, e un valore booleano utilizzato per indicare se è necessario cancellare la memoria. È la funzione di ingresso per tutte le allocazioni di memoria di oggetti in Go. Quando si utilizza la funzione new per creare un puntatore, si entra anche in questa funzione. Quando l'allocazione della memoria ha successo, il puntatore restituito è l'indirizzo dell'oggetto. Nella parte mspan è stato menzionato che ogni mspan possiede un spanClass, e spanClass determina la dimensione fissa di mspan. Go divide l'intervallo di oggetti da [0, 32KB] in 68 dimensioni diverse, quindi la memoria di Go è composta da diverse liste concatenate mspan di dimensioni fisse. Quando si alloca memoria per un oggetto, basta calcolare il spanClass corrispondente in base alla dimensione dell'oggetto, poi trovare la lista concatenata mspan corrispondente in base a spanClass, e infine cercare un mspan disponibile dalla lista concatenata. Questo approccio gerarchico può risolvere efficacemente il problema della frammentazione della memoria.

Micro Oggetti
Tutti i micro oggetti non puntatore inferiori a 16B vengono allocati dall'allocatore micro in P nella stessa memoria continua. In runtime.mcache, il campo tiny registra l'indirizzo base di questa memoria.
type mcache struct {
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
}La dimensione dei micro oggetti è determinata dalla costante runtime.maxTinySize, che è 16B. Il blocco di memoria utilizzato per memorizzare i micro oggetti è della stessa dimensione. Generalmente, gli oggetti memorizzati qui sono alcune piccole stringhe. La parte di codice responsabile dell'allocazione dei micro oggetti è la seguente:
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 = maxTinySizeSe il blocco di memoria micro corrente ha abbastanza spazio per contenere, utilizza direttamente il blocco di memoria corrente, cioè off+size <= maxTinySize. Se non c'è abbastanza spazio, prova prima a cercare spazio disponibile dalla cache span di mcache. Se nemmeno questo è sufficiente, richiede un mspan a mcentral. Indipendentemente da come, alla fine si ottiene un indirizzo disponibile, poi si sostituisce il vecchio blocco di memoria micro con il nuovo.
Piccoli Oggetti
La maggior parte degli oggetti nel runtime del linguaggio Go sono piccoli oggetti nell'intervallo [16B, 32KB]. Il processo di allocazione dei piccoli oggetti è il più complicato, ma il codice è il meno. La parte di codice responsabile dell'allocazione dei piccoli oggetti è la seguente:
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)
}Prima calcola in base alla dimensione dell'oggetto quale spanClass dovrebbe essere utilizzato, poi runtime.nextFreeFast prova a ottenere spazio di memoria disponibile dal mspan cache corrispondente in mcache in base a spanClass.
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
}La funzione di mspan.allocCache è registrare se lo spazio di memoria è utilizzato da oggetti, ed è diviso in base al numero di oggetti piuttosto che in base alla dimensione dello spazio. Questo equivale a considerare mspan come un array di oggetti, come mostrato nella figura seguente:

allocCache è un numero a 64 bit, e ogni bit corrisponde a uno spazio di memoria. Se un bit è 0, indica che lo spazio di memoria è utilizzato da un oggetto. Se è 1, indica che lo spazio di memoria è idle. Lo scopo di sys.TrailingZeros64(s.allocCache) è calcolare il numero di zeri finali. Se il risultato è 64, indica che non c'è memoria idle disponibile. Se c'è memoria disponibile, calcola l'offset della memoria idle e lo aggiunge all'indirizzo base di mspan e restituisce.
Quando mcache non ha memoria sufficiente, richiede a mcentral. Questo lavoro è completato dal metodo 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
}Dove mcache.refill è responsabile della richiesta di un mspan disponibile a mcentral:
func (c *mcache) refill(spc spanClass) {
...
s = mheap_.central[spc].mcentral.cacheSpan()
...
}E il metodo mcentral.cacheSpan esegue l'espansione tramite mcentral.grow quando la memoria è insufficiente. L'espansione richiede un nuovo mspan a mheap:
func (c *mcentral) grow() *mspan {
...
s := mheap_.alloc(npages, c.spanclass)
...
return s
}Quindi, in definitiva, l'allocazione della memoria per i piccoli oggetti procede livello per livello: prima mcache, poi mcentral, infine mheap. Il costo di allocazione di mcache è il più basso, poiché è la cache locale del P e non richiede un lock durante l'allocazione della memoria. mcentral è il secondo, e richiedere direttamente memoria a mheap ha il costo più alto, poiché il metodo mheap.alloc compete per il lock globale dell'intera heap.
Grandi Oggetti
L'allocazione di grandi oggetti è la più semplice. Se la dimensione dell'oggetto supera 32KB, richiede direttamente a mheap di allocare un nuovo mspan per contenerlo. La parte di codice responsabile dell'allocazione di grandi oggetti è la seguente:
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)
}
}Dove mcache.allocLarge è responsabile della richiesta di memoria per grandi oggetti a mheap:
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
...
spc := makeSpanClass(0, noscan)
s := mheap_.alloc(npages, spc)
...
return s
}Dal codice si può vedere che il valore di spanClass utilizzato dai grandi oggetti è 0, e fondamentalmente un grande oggetto occupa un mspan.
Altro
Statistiche di Memoria
Il runtime di Go espone agli utenti una funzione ReadMemStats, che può essere utilizzata per statistiche sulla memoria del 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()
}Ma il costo di utilizzarla è molto alto. Dal codice si può vedere che è necessario STW prima di analizzare la situazione della memoria, e la durata di STW può variare da pochi millisecondi a centinaia di millisecondi. Generalmente viene utilizzata solo durante il debug e la risoluzione dei problemi. La struttura runtime.MemStats registra informazioni relative alla memoria heap, alla memoria stack e al 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
L'allocatore di memoria è ovviamente utilizzato per allocare memoria heap, ma la heap è divisa in due parti: una parte è la memoria heap necessaria per il runtime di Go stesso, e l'altra parte è la memoria heap aperta agli utenti. Quindi in alcune strutture si può vedere questo campo incorporato:
_ sys.NotInHeapIndica che la memoria di questo tipo non viene allocata sulla heap utente. Questo campo incorporato è particolarmente comune nei componenti dell'allocatore di memoria, come la struttura che rappresenta la heap utente runtime.mheap:
type mheap struct {
_ sys.NotInHeap
}Il vero scopo di sys.NotInHeap è evitare la barriera di memoria per migliorare l'efficienza del runtime, mentre la heap utente deve eseguire il GC quindi richiede la barriera di memoria.
