memory
A diferencia del c/c++ tradicional, go es un lenguaje con gc, en la mayoría de los casos la asignación y destrucción de memoria es gestionada automáticamente por go. La decisión de si la memoria de un objeto debe asignarse en el stack o en el heap la toma el compilador. Básicamente no se requiere que el usuario participe en la gestión de memoria, lo que el usuario debe hacer es simplemente usar la memoria. En go, la gestión de memoria del heap tiene dos componentes principales: el asignador de memoria es responsable de asignar memoria del heap, y el recolector de basura es responsable de liberar y reciclar la memoria del heap que ya no se usa. Este artículo se centra principalmente en cómo funciona el asignador de memoria. El asignador de memoria de go está muy influenciado por el asignador de memoria TCMalloc de Google.
Asignador
En go hay dos tipos de asignadores de memoria: uno es el asignador lineal y el otro es el asignador enlazado.
Asignación Lineal
El asignador lineal corresponde a la estructura runtime.linearAlloc, como se muestra a continuación:
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
}Este asignador solicita al sistema operativo un espacio de memoria continuo, next apunta a la dirección de memoria disponible, end apunta a la dirección final del espacio de memoria, lo que puede entenderse aproximadamente como se muestra en la siguiente figura.

La forma de asignación de memoria del asignador lineal es muy fácil de entender. Según el tamaño de memoria solicitado, verifica si hay suficiente espacio restante para容纳. Si es suficiente, actualiza el campo next y retorna la dirección de inicio del espacio restante. El código es el siguiente:
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)
}La ventaja de este método de asignación es que es rápido y simple. La desventaja también es bastante obvia: no puede reutilizar la memoria liberada, porque el campo next solo apunta a la dirección de memoria del espacio restante, y no puede percibir el espacio de memoria que se usó y luego se liberó. Esto causará un gran desperdicio de espacio de memoria, como se muestra en la siguiente figura.

Por lo tanto, la asignación lineal no es el método principal en go. Solo se usa como función de pre-asignación de memoria en máquinas de 32 bits.
Asignación Enlazada
El asignador enlazado corresponde a la estructura runtime.fixalloc. La memoria asignada por el asignador enlazado no es continua y existe en forma de una lista enlazada simple. El asignador enlazado está compuesto por varios bloques de memoria de tamaño fijo, y cada bloque de memoria está compuesto por varios fragmentos de memoria de tamaño fijo. Cada vez que se realiza una asignación de memoria, se usa un fragmento de memoria de tamaño fijo.
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
}Sus campos no son tan simples y fáciles de entender como los del asignador lineal. Aquí se presenta una introducción simple a los importantes:
size: se refiere a cuánta memoria se usa cada vez que se asigna memoria.list: apunta al nodo inicial de la lista de fragmentos de memoria reutilizables. El tamaño de cada fragmento de memoria está determinado porsize.chunk: apunta a la dirección libre del bloque de memoria actual que se está usando.nchunk: número de bytes disponibles restantes del bloque de memoria actual.nalloc: tamaño del bloque de memoria, fijo en 16KB.inuse: total de bytes de memoria que se han usado.zero: si se debe limpiar la memoria al reutilizar un bloque de memoria.
El asignador enlazado mantiene referencias al bloque de memoria actual y a los fragmentos de memoria reutilizables. El tamaño de cada bloque de memoria es fijo en 16KB. Este valor se establece durante la inicialización.
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 distribución de los bloques de memoria se muestra en la siguiente figura. Los bloques de memoria en la figura están ordenados por tiempo de creación. En realidad, sus direcciones no son continuas.

El tamaño de memoria asignado cada vez por el asignador enlazado también es fijo, determinado por fixalloc.size. Al asignar, primero verifica si hay un bloque de memoria reutilizable. Si lo hay, prioriza el uso del bloque de memoria reutilizable. Luego usa el bloque de memoria actual. Si el espacio restante del bloque de memoria actual no es suficiente para容纳, crea un nuevo bloque de memoria. Esta parte de la lógica corresponde al siguiente código:
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
}La ventaja del asignador enlazado es que puede reutilizar la memoria liberada. La unidad básica para reutilizar memoria es un fragmento de memoria de tamaño fijo, cuyo tamaño está determinado por fixalloc.size. Al liberar memoria, el asignador enlazado agrega el fragmento de memoria como nodo inicial a la lista de fragmentos de memoria libres. El código es el siguiente:
func (f *fixalloc) free(p unsafe.Pointer) {
f.inuse -= f.size
v := (*mlink)(p)
v.next = f.list
f.list = v
}Componentes de Memoria
El asignador de memoria en go está compuesto principalmente por los componentes mspan, heaparena, mcache, mcentral y mheap. Estos componentes trabajan en capas para gestionar toda la memoria del heap en go.
mspan

runtime.mspan es la unidad básica en la asignación de memoria de go. Su estructura es la siguiente:
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 y mspan están enlazados en forma de lista enlazada doble a través de next y prev. Las direcciones de memoria no son continuas. Cada mspan gestiona mspan.npages páginas de memoria de tamaño runtime.pageSize. Generalmente, el tamaño de página es 8KB. mspan.startAddr registra la dirección de inicio de estas páginas y mspan.limit registra la dirección final de la memoria usada. El tamaño de los elementos almacenados en cada mspan (elemsize) es fijo, por lo que la cantidad de elementos que puede容纳 también es fija. Debido a que la cantidad es fija, los objetos se distribuyen en mspan como un array, en el rango [0, nelems]. freeindex registra el índice del próximo objeto disponible para almacenar. mspan tiene un total de tres estados:
- mSpanDead: la memoria ha sido liberada
- mSpanInUse: asignado al heap
- mSpanManual: asignado a la parte de memoria gestionada manualmente, como el stack.
Lo que determina el tamaño del elemento de mspan es spanClass. spanClass es un entero de tipo uint8. Los siete bits altos almacenan el valor class que representa 0-67. El último bit se usa para indicar noscan, es decir, si contiene punteros.
type spanClass uint8
func (sc spanClass) sizeclass() int8 {
return int8(sc >> 1)
}
func (sc spanClass) noscan() bool {
return sc&1 != 0
}Tiene un total de 68 valores diferentes. Todos los valores se almacenan en forma de tabla en el archivo runtime.sizeclasses.go. En tiempo de ejecución, se usa spanClass a través de runtime.class_to_size para obtener el tamaño del objeto de mspan, y a través de class_to_allocnpages se puede obtener el número de páginas de mspan.
| class | Tamaño máximo de objeto | Tamaño de span | Cantidad de objetos | Desperdicio de cola | Tasa máxima de desperdicio de memoria | Alineación mínima |
|---|---|---|---|---|---|---|
| 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 lógica de cálculo de estos valores se puede encontrar en la función printComment en runtime.mksizeclasses.go. La fórmula de cálculo de la tasa máxima de desperdicio de memoria es:
float64((size-prevSize-1)*objects+tailWaste) / float64(spanSize)Por ejemplo, cuando class es 2, su tasa máxima de desperdicio de memoria es:
((16-8-1)*512+0)/8192 = 0.4375Cuando el valor de class es 0, es el spanClass usado específicamente para asignar objetos grandes mayores a 32KB. Básicamente, un objeto grande ocupa un mspan. Por lo tanto, el heap de go está compuesto en realidad por varias listas enlazadas de mspan de diferentes tamaños fijos.
heaparena

Como se mencionó anteriormente, mspan está compuesto por varias páginas, pero mspan solo mantiene la referencia de dirección de las páginas y no es responsable de gestionar estas páginas. El verdadero responsable de gestionar estas páginas de memoria es runtime.heaparena. Cada heaparena gestiona varias páginas. El tamaño de heaparena está determinado por runtime.heapArenaBytes, generalmente 64MB. bitmap se usa para identificar si las direcciones correspondientes en la página almacenan objetos. zeroedBase es la dirección de inicio de la página de memoria gestionada por este heaparena. spans registra qué mspan usa cada página.
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 lógica relacionada con el registro de páginas y mspan se puede encontrar en el método mheap.setSpans, como se muestra a continuación:
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
}
}En el heap de go, todas las páginas de memoria son gestionadas por un array bidimensional de heaparena. Ver el campo mheap.arenas:
type mheap struct {
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
}En la plataforma windows de 64 bits, la primera dimensión del array es 1 << 6, la segunda dimensión es 1 << 16. En la plataforma linux de 64 bits, la primera dimensión es 1, la segunda dimensión es 1 << 22. Este array bidimensional compuesto por todos los heaparena constituye el espacio de memoria virtual del tiempo de ejecución de go. En general, se ve como se muestra en la siguiente figura.

Aunque los heaparena están adyacentes entre sí, las páginas de memoria que gestionan no son continuas.
mcache
mcache corresponde a la estructura runtime.mcache. Ya apareció en el artículo de planificación de concurrencia. Aunque su nombre es mcache, en realidad está vinculado al procesador P. mcache es la caché de memoria de cada procesador P. Contiene el array de listas enlazadas de mspan alloc. El tamaño del array es fijo en 136, que es exactamente el doble de la cantidad de spanClass. También contiene la caché de micro objetos tiny. tiny apunta a la dirección de inicio de la memoria de micro objetos. tinyoffset es el offset de memoria libre relativo a la dirección de inicio. tinyAllocs indica cuántos micro objetos se han asignado. Para la caché de stack stackcache, se puede consultar en Asignación de Memoria de 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
}Al inicializar, las listas enlazadas en alloc de mcache solo contienen un nodo de cabecera vacío runtime.emptymspan, es decir, no hay mspan con memoria disponible.
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 cuando se necesita asignar memoria, se solicita un nuevo mspan a mcentral para reemplazar el span vacío original. Este trabajo lo completa el método mcache.refill. Su única entrada de llamada es la función runtime.mallocgc. A continuación se muestra el código simplificado:
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
}La ventaja de usar mcache es que no se requiere un lock global al asignar memoria. Sin embargo, cuando la memoria es insuficiente, se necesita acceder a mcentral, y en ese momento todavía se requiere un lock.
mcentral
runtime.mcentral gestiona todos los mspan en el heap que almacenan objetos pequeños. Cuando mcache solicita memoria, también es asignado por mcentral.
type mcentral struct {
_ sys.NotInHeap
spanclass spanClass
partial [2]spanSet
full [2]spanSet
}Los campos de mcentral son pocos. spanClass indica el tipo de mspan almacenado. partial y full son dos spanSet. El primero almacena mspan con memoria libre, y el segundo almacena mspan sin memoria libre. mcentral es gestionado directamente por el heap mheap. En tiempo de ejecución, hay un total de 136 mcentral.
type mheap struct {
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
}mcentral es principalmente responsable de dos trabajos: asignar mspan disponible a mcache cuando hay suficiente memoria, y solicitar a mheap que asigne un nuevo mspan cuando la memoria es insuficiente. El trabajo de asignar mspan a mcache lo completa el método mcentral.cacheSpan. Primero intenta buscar mspan disponible en el conjunto barrido de la lista libre.
// Try partial swept spans first.
sg := mheap_.sweepgen
if s = c.partialSwept(sg).pop(); s != nil {
goto havespan
}Si no se encuentra, busca mspan disponible en el conjunto no barrido de la lista libre.
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
}
}Si aún no se encuentra, busca en el conjunto no barrido de la lista no libre.
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)
}
}Si finalmente no se encuentra, el método mcentral.grow solicita a mheap que asigne un nuevo mspan.
s = c.grow()
if s == nil {
return nil
}En condiciones normales, siempre se retorna un mspan disponible.
havespan:
freeByteBase := s.freeindex &^ (64 - 1)
whichByte := freeByteBase / 8
// Init alloc bits cache.
s.refillAllocCache(whichByte)
s.allocCache >>= s.freeindex % 64
return sPara el proceso de solicitar mspan a mheap, en realidad se llama al método mheap.alloc, que retorna un nuevo 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
}Después de inicializarlo correctamente, se puede asignar a mcache para su uso.
mheap

runtime.mheap es el gestor de la memoria del heap en go. En tiempo de ejecución, existe como variable global runtime.mheap_.
var mheap_ mheapGestiona todos los mspan creados, todos los mcentral, todos los heaparena, y muchos otros asignadores variados. Su estructura simplificada es la siguiente:
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
}Para mheap, en tiempo de ejecución hay principalmente cuatro trabajos por hacer:
- Inicializar el heap
- Asignar
mspan - Liberar
mspan - Expandir el heap
A continuación se explican estas cuatro cosas en orden.
Inicialización
El período de inicialización del heap está en la fase de arranque del programa, que también es la fase de inicialización del planificador. El orden de llamada es:
schedinit() -> mallocinit() -> mheap_.init()Durante la inicialización, es principalmente responsable de ejecutar el trabajo de inicialización de varios asignadores:
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)
}Esto incluye el asignador mheap.spanalloc responsable de asignar mspan, el asignador mheap.pages responsable de asignar páginas, y la inicialización de todos los mcentral.
Asignación
En mheap, la asignación de mspan la completa el método mheap.allocSpan:
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan)Si el tamaño de memoria solicitado es lo suficientemente pequeño, es decir, cumple npages < pageCachePages/4, intenta obtener un mspan disponible de la caché mspan del P local sin lock. Si la caché P está vacía, primero la inicializa:
// If the cache is empty, refill it.
if c.empty() {
lock(&h.lock)
*c = h.pages.allocToCache()
unlock(&h.lock)
}Luego obtiene de la caché P, completado por el método 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
}
}
}El código para obtener mspan de la caché P es el siguiente. Intenta obtener el último mspan de la caché:
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
}Si la memoria solicitada es relativamente grande, se asigna memoria en el heap. Durante este proceso se requiere un 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)Primero usa pageAlloc.alloc para asignar suficiente memoria de página. Si la memoria del heap no es suficiente, mheap.grow realiza la expansión. Después de completar la asignación de memoria de página, el asignador enlazado mheap.spanalloc asigna 64 mspan a la caché local P (64 es exactamente la mitad de la longitud del array de caché). Luego retorna un mspan disponible de la caché 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
}Según las dos situaciones anteriores, finalmente se puede obtener un mspan disponible. Finalmente, después de inicializar mspan, se puede retornar:
HaveSpan:
h.initSpan(s, typ, spanclass, base, npages)
return sLiberación
Dado que mspan es asignado por el asignador enlazado, naturalmente la liberación de memoria también la realiza él.
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)
}Primero marca la página de memoria especificada como liberada a través del asignador de páginas mheap.pages, luego establece el estado de mspan a mSpanDead, y finalmente el asignador mheap.spanalloc libera 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))
}Si la caché P no está llena, se coloca en la caché local P para continuar usándola. De lo contrario, se libera de vuelta a la memoria del heap.
Expansión
El espacio de memoria de página gestionado por heaparena no se solicita completamente en la etapa inicial. Solo se asigna cuando se necesita usar memoria. El método responsable de expandir la memoria del heap es mheap.grow. A continuación se muestra el código simplificado:
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)
}
...
}Primero calcula la memoria requerida según npage y la alinea. Luego determina si el heaparena actual tiene suficiente memoria. Si no es suficiente, mheap.sysAlloc solicita más memoria para el heaparena actual o asigna un nuevo 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
}
}
...
}Primero intenta usar el asignador lineal mheap.arena para solicitar un bloque de memoria en el espacio de memoria pre-asignado. Si falla, realiza la expansión según hintList. El tipo de hintList es runtime.arenaHint, que registra específicamente la información de dirección relacionada con la expansión de 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))
}Después de solicitar la memoria, se actualiza en el array bidimensional 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))
}Finalmente, el asignador de páginas marca esta memoria como estado listo:
// Update the page allocator's structures to make this
// space ready for allocation.
h.pages.grow(v, nBase-v)
totalGrowth += nBase - vAsignación de Objetos
Cuando go asigna memoria para objetos, los divide en tres tipos diferentes según el tamaño:
- Micro objetos - tiny, menor que 16B
- Objetos pequeños - small, menor que 32KB
- Objetos grandes - large, mayor que 32KB
Según los tres tipos diferentes, se ejecuta lógica diferente al asignar memoria. La función responsable de asignar memoria para objetos es runtime.mallocgc. Su firma de función es la siguiente:
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.PointerSolo tiene tres parámetros: tamaño de memoria, tipo, y un valor booleano que indica si se necesita limpiar la memoria. Es la función de entrada para todas las asignaciones de memoria de objetos en go. Al usar la función new para crear un puntero, también se entra en esta función. Cuando la asignación de memoria es exitosa, el puntero retornado es la dirección del objeto. En la parte mspan se mencionó que cada mspan tiene un spanClass. spanClass determina el tamaño fijo de mspan. go divide el rango de objetos de [0, 32KB] en 68 tamaños diferentes. Por lo tanto, la memoria de go está compuesta por varias listas enlazadas de mspan de tamaños fijos diferentes. Al asignar memoria de objetos, solo se necesita calcular el spanClass correspondiente según el tamaño del objeto, luego encontrar la lista enlazada de mspan correspondiente según spanClass, y finalmente buscar un mspan disponible en la lista enlazada. Esta práctica jerárquica puede resolver el problema de fragmentación de memoria de manera más efectiva.

Micro Objetos
Todos los micro objetos no puntero menores a 16B son asignados por el micro asignador en P al mismo espacio de memoria continuo. En runtime.mcache, el campo tiny registra la dirección base de esta memoria.
type mcache struct {
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
}El tamaño de los micro objetos está determinado por la constante runtime.maxTinySize, que es 16B. El bloque de memoria usado para almacenar micro objetos es del mismo tamaño. Generalmente, los objetos almacenados aquí son algunas cadenas pequeñas. El código de la parte responsable de asignar micro objetos es el siguiente:
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 = maxTinySizeSi el bloque de micro memoria actual tiene suficiente espacio para容纳, usa directamente el bloque de memoria actual, es decir, off+size <= maxTinySize. Si no es suficiente, primero intenta buscar espacio disponible en la caché span de mcache. Si tampoco es posible, solicita un mspan a mcentral. De cualquier manera, finalmente se obtiene una dirección disponible. Finalmente, se reemplaza el bloque de memoria de micro objetos antiguo con uno nuevo.
Objetos Pequeños
La mayoría de los objetos en el tiempo de ejecución de go son objetos pequeños en el rango [16B, 32KB]. El proceso de asignación de objetos pequeños es el más problemático, pero el código es el más mínimo. La parte del código responsable de la asignación de objetos pequeños es la siguiente:
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)
}Primero calcula qué spanClass se debe usar según el tamaño del objeto. Luego runtime.nextFreeFast intenta obtener espacio de memoria disponible del mspan de caché correspondiente en mcache según 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 función de mspan.allocCache es registrar si el espacio de memoria está siendo usado por un objeto. Está dividido por cantidad de objetos en lugar de por tamaño de espacio. Esto es equivalente a ver mspan como un array de objetos, como se muestra en la siguiente figura.

allocCache es un número de 64 bits. Cada bit corresponde a un espacio de memoria. Si un bit es 0, indica que el espacio de memoria está siendo usado por un objeto. Si es 1, indica que el espacio de memoria está libre. El propósito de sys.TrailingZeros64(s.allocCache) es calcular la cantidad de ceros finales. Si el resultado es 64, indica que no hay memoria libre disponible. Si la hay, se calcula el offset de memoria libre y se suma a la dirección base de mspan y se retorna.
Cuando mcache no tiene suficiente espacio, se solicita a mcentral. Este trabajo lo completa el método 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
}mcache.refill es responsable de solicitar un mspan disponible a mcentral.
func (c *mcache) refill(spc spanClass) {
...
s = mheap_.central[spc].mcentral.cacheSpan()
...
}El método mcentral.cacheSpan realiza la expansión a través de mcentral.grow cuando la memoria es insuficiente. La expansión solicita un nuevo mspan a mheap.
func (c *mcentral) grow() *mspan {
...
s := mheap_.alloc(npages, c.spanclass)
...
return s
}Por lo tanto, al final, la asignación de memoria de objetos pequeños va nivel por nivel: primero mcache, luego mcentral, y finalmente mheap. El costo de asignación de mcache es el más bajo, porque es la caché local P y no se requiere un lock al asignar memoria. mcentral es el siguiente. Solicitar memoria directamente a mheap tiene el costo más alto, porque el método mheap.alloc compite por el lock global del heap.
Objetos Grandes
La asignación de objetos grandes es la más simple. Si el tamaño del objeto excede 32KB, solicita directamente a mheap que asigne un nuevo mspan para容纳. El código de la parte responsable de asignar objetos grandes es el siguiente:
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)
}
}mcache.allocLarge es responsable de solicitar memoria de objetos grandes a mheap:
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
...
spc := makeSpanClass(0, noscan)
s := mheap_.alloc(npages, spc)
...
return s
}Se puede ver en el código que el valor de spanClass usado por los objetos grandes es 0. Básicamente, un objeto grande ocupa un mspan.
Otros
Estadísticas de Memoria
El tiempo de ejecución de go expone una función ReadMemStats a los usuarios, que se puede usar para estadísticas de memoria del tiempo de ejecución.
func ReadMemStats(m *MemStats) {
_ = m.Alloc // nil check test before we switch stacks, see issue 61158
stopTheWorld(stwReadMemStats)
systemstack(func() {
readmemstats_m(m)
})
startTheWorld()
}Pero el costo de usarlo es muy alto. Se puede ver en el código que se requiere STW antes de analizar la situación de memoria. La duración de STW puede ser de varios milisegundos a varios cientos de milisegundos. Generalmente solo se usa durante la depuración y solución de problemas. La estructura runtime.MemStats registra información relacionada con la memoria del heap, memoria del stack, y 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
El asignador de memoria obviamente se usa para asignar memoria del heap, pero el heap se divide en dos partes: una es la memoria del heap requerida por el propio tiempo de ejecución de go, y la otra es la memoria del heap disponible para los usuarios. Por lo tanto, en algunas estructuras se puede ver tal campo incrustado:
_ sys.NotInHeapIndica que la memoria de este tipo no se asignará en el heap de usuario. Este tipo de campo incrustado es especialmente común en los componentes de asignación de memoria, como la estructura runtime.mheap que representa el heap de usuario:
type mheap struct {
_ sys.NotInHeap
}El verdadero propósito de sys.NotInHeap es evitar la barrera de memoria para mejorar la eficiencia del tiempo de ejecución. El heap de usuario necesita ejecutar GC, por lo que requiere una barrera de memoria.
