Skip to content

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:

go
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:

go
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.

go
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 por size.
  • 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.

go
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:

go
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:

go
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:

go
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.

go
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.

classTamaño máximo de objetoTamaño de spanCantidad de objetosDesperdicio de colaTasa máxima de desperdicio de memoriaAlineación mínima
1881921024087.50%8
2168192512043.75%16
3248192341829.24%8
4328192256021.88%32
54881921703231.52%16
6648192128023.44%64
78081921023219.07%16
8968192853215.95%32
91128192731613.56%16
10128819264011.72%128
1114481925612811.82%16
12160819251329.73%32
13176819246969.59%16
141928192421289.25%64
15208819239808.12%16
162248192361288.15%32
17240819234326.62%16
1825681923205.86%256
1928881922812812.16%32
2032081922519211.80%64
21352819223969.88%32
223848192211289.51%128
2341681921928810.71%32
244488192181288.37%64
25480819217326.82%32
2651281921606.05%512
2757681921412812.33%64
2864081921251215.48%128
2970481921144813.93%64
3076881921051213.94%256
318968192912815.52%128
32102481928012.40%1024
3311528192712812.41%128
3412808192651215.55%256
351408163841189614.00%128
3615368192551214.00%512
37179216384925615.57%256
38204881924012.45%2048
39230416384725612.46%256
4026888192312815.59%128
413072245768012.47%1024
4232001638453846.22%128
4334562457673848.83%128
44409681922015.60%4096
45486424576525616.65%256
46537616384325610.92%256
476144245764012.48%2048
4865283276851286.23%128
4967844096062564.36%128
5069124915277683.37%256
51819281921015.61%8192
52947257344651214.28%256
5397284915255123.64%512
541024040960404.99%2048
55108803276831286.24%128
5612288245762011.45%4096
57135684096032569.99%256
581433657344405.35%2048
5916384163841012.49%8192
6018432737284011.11%2048
61190725734431283.57%128
622048040960206.87%4096
63217606553632566.25%256
6424576245761011.45%8192
652726481920312810.00%128
662867257344204.91%4096
6732768327681012.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:

go
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.4375

Cuando 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.

go
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:

go
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:

go
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.

go
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.

go
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:

go
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.

go
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.

go
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.

go
// 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.

go
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.

go
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.

go
s = c.grow()
if s == nil {
    return nil
}

En condiciones normales, siempre se retorna un mspan disponible.

go
havespan:
  freeByteBase := s.freeindex &^ (64 - 1)
  whichByte := freeByteBase / 8
  // Init alloc bits cache.
  s.refillAllocCache(whichByte)
  s.allocCache >>= s.freeindex % 64

  return s

Para el proceso de solicitar mspan a mheap, en realidad se llama al método mheap.alloc, que retorna un nuevo mspan.

go
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_.

go
var mheap_ mheap

Gestiona todos los mspan creados, todos los mcentral, todos los heaparena, y muchos otros asignadores variados. Su estructura simplificada es la siguiente:

go
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:

go
schedinit() -> mallocinit() -> mheap_.init()

Durante la inicialización, es principalmente responsable de ejecutar el trabajo de inicialización de varios asignadores:

go
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:

go
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:

go
// 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:

go
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é:

go
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:

go
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:

go
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:

go
HaveSpan:
  h.initSpan(s, typ, spanclass, base, npages)
  return s

Liberación

Dado que mspan es asignado por el asignador enlazado, naturalmente la liberación de memoria también la realiza él.

go
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.

go
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:

go
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.

go
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.

go
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:

go
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:

go
// Update the page allocator's structures to make this
// space ready for allocation.
h.pages.grow(v, nBase-v)
totalGrowth += nBase - v

Asignació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:

go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer

Solo 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.

go
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:

go
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 = maxTinySize

Si 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:

go
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.

go
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:

go
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.

go
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.

go
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:

go
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:

go
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.

go
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:

go
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:

go
_ sys.NotInHeap

Indica 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:

go
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.

Golang editado por www.golangdev.cn