Skip to content

memory

Diferentemente do C/C++ tradicional, Go é uma linguagem com GC, onde na maioria dos casos a alocação e liberação de memória é gerenciada automaticamente pelo Go. O compilador decide se a memória de um objeto deve ser alocada na stack ou no heap, basicamente sem necessidade de intervenção do usuário. O usuário apenas usa a memória. Em Go, o gerenciamento de memória do heap tem dois componentes principais: o alocador de memória负责 alocação de memória do heap, e o coletor de lixo负责回收 e liberação de memória de heap não utilizada. Este artigo foca principalmente no funcionamento do alocador de memória. O alocador de memória do Go é fortemente influenciado pelo TCMalloc (Thread-Caching Malloc) do Google.

Alocador

Em Go, existem dois tipos de alocadores de memória: alocador linear e alocador em cadeia.

Alocação Linear

O alocador linear corresponde à estrutura runtime.linearAlloc, conforme mostrado abaixo:

go
type linearAlloc struct {
  next   uintptr // próximo byte disponível
  mapped uintptr // um byte após o fim do espaço mapeado
  end    uintptr // fim do espaço reservado

  mapMemory bool // transiciona memória de Reserved para Ready se true
}

Este alocador solicita antecipadamente do sistema operacional um espaço de memória contíguo. next aponta para o endereço de memória disponível, end aponta para o endereço final do espaço de memória, que pode ser entendido conforme o diagrama abaixo:

O método de alocação de memória do alocador linear é muito fácil de entender: verifica-se se há espaço restante suficiente para acomodar de acordo com o tamanho da memória solicitada. Se houver espaço suficiente, atualiza-se o campo next e retorna-se o endereço inicial do espaço restante. O código é conforme abaixo:

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)
}

A vantagem deste método de alocação é ser rápido e simples. A desvantagem também é bastante óbvia: não é possível reutilizar memória já liberada, pois o campo next só aponta para o endereço de memória do espaço restante, não sendo possível perceber o espaço de memória já utilizado e depois liberado. Isso causa grande desperdício de espaço de memória, conforme mostrado no diagrama abaixo:

Portanto, alocação linear não é o principal método de alocação em Go. Ela é usada apenas como funcionalidade de pré-alocação de memória em máquinas de 32 bits.

Alocação em Cadeia

O alocador em cadeia corresponde à estrutura runtime.fixalloc. A memória alocada pelo alocador em cadeia não é contígua, existindo na forma de lista encadeada unidirecional. O alocador em cadeia é composto por vários blocos de memória de tamanho fixo, e cada bloco de memória é composto por vários fragmentos de memória de tamanho fixo. Cada vez que é realizada alocação de memória, é usado um fragmento de memória de tamanho fixo.

go
type fixalloc struct {
  size   uintptr
  first  func(arg, p unsafe.Pointer) // chamado na primeira vez que p é retornado
  arg    unsafe.Pointer
  list   *mlink
  chunk  uintptr // usa uintptr ao invés de unsafe.Pointer para evitar barreiras de escrita
  nchunk uint32  // bytes restantes no chunk atual
  nalloc uint32  // tamanho de novos chunks em bytes
  inuse  uintptr // bytes em uso atualmente
  stat   *sysMemStat
  zero   bool // zerar alocações
}

type mlink struct {
  _    sys.NotInHeap
  next *mlink
}

Seus campos não são tão simples e fáceis de entender quanto o alocador linear. Aqui apresentamos brevemente os importantes:

  • size: refere-se a quanto memória é usada cada vez que é realizada alocação de memória
  • list: aponta para o nó cabeça da lista de fragmentos de memória reutilizáveis; o tamanho de cada fragmento de memória é determinado por size
  • chunk: aponta para o endereço ocioso no bloco de memória atualmente em uso
  • nchunk: número de bytes disponíveis restantes no bloco de memória atual
  • nalloc: tamanho do bloco de memória, fixo em 16KB
  • inuse: total de bytes de memória já utilizados
  • zero: ao reutilizar bloco de memória, se a memória deve ser zerada

O alocador em cadeia mantém referência ao bloco de memória atual e aos fragmentos de memória reutilizáveis. O tamanho de cada bloco de memória é fixo em 16KB, valor que é definido durante inicialização.

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
}

A distribuição dos blocos de memória é conforme mostrado no diagrama abaixo. Os blocos de memória no diagrama estão organizados por ordem de tempo de criação. Na realidade, seus endereços não são contíguos.

O tamanho de memória alocado cada vez pelo alocador em cadeia também é fixo, determinado por fixalloc.size. Durante alocação, primeiro verifica-se se há bloco de memória reutilizável. Se houver, prioriza-se o uso do bloco de memória reutilizável. Caso contrário, usa-se o bloco de memória atual. Se o espaço restante do bloco de memória atual não for suficiente para acomodar, cria-se um novo bloco de memória. Esta parte da lógica corresponde ao código abaixo:

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
}

A vantagem do alocador em cadeia é justamente poder reutilizar memória liberada. A unidade básica para reutilizar memória é um fragmento de memória de tamanho fixo, cujo tamanho é determinado por fixalloc.size. Ao liberar memória, o alocador em cadeia adiciona este fragmento de memória como nó cabeça à lista de fragmentos de memória ociosos. O código é conforme mostrado abaixo:

go
func (f *fixalloc) free(p unsafe.Pointer) {
  f.inuse -= f.size
  v := (*mlink)(p)
  v.next = f.list
  f.list = v
}

Componentes de Memória

O alocador de memória em Go é composto principalmente pelos componentes mspan, heaparena, mcache, mcentral, mheap. Eles atuam em camadas, gerenciando toda a memória heap do Go.

mspan

runtime.mspan é a unidade básica na alocação de memória do Go. Sua estrutura é conforme abaixo:

go
type mspan struct {
    next *mspan     // próximo span na lista, ou nil se nenhum
    prev *mspan     // span anterior na lista, ou nil se nenhum

    startAddr uintptr // endereço do primeiro byte do span, também conhecido como s.base()
    npages    uintptr // número de páginas no span
    freeindex uintptr

    spanclass             spanClass     // classe de tamanho e noscan (uint8)
    needzero              uint8         // precisa ser zerado antes da alocação
    elemsize              uintptr       // computado a partir de sizeclass ou de npages
    limit                 uintptr       // fim dos dados no span
    state                 mSpanStateBox // mSpanInUse etc; acessado atomicamente (métodos get/set)

    nelems uintptr // número de objetos no span
    allocCache uint64
    allocCount            uint16        // número de objetos alocados
    ...
}

mspan e mspan são linkados na forma de lista encadeada bidirecional através de next e prev. Os endereços de memória não são contíguos. Cada mspan gerencia mspan.npages páginas de memória de tamanho runtime.pageSize. Normalmente, o tamanho da página é 8KB. mspan.startAddr registra o endereço inicial destas páginas, e mspan.limit registra o endereço final da memória já utilizada. O tamanho dos elementos armazenados em cada mspan (elemsize) é fixo, então a quantidade de elementos que pode acomodar também é fixa. Devido à quantidade fixa, o armazenamento de objetos é como um array distribuído em mspan, no intervalo [0, nelems]. freeindex registra o próximo índice disponível para armazenar objetos. mspan tem três estados no total:

  • mSpanDead: memória já foi liberada
  • mSpanInUse: alocado no heap
  • mSpanManual: alocado para parte de gerenciamento manual de memória, como stack

O que determina o tamanho dos elementos de mspan é spanClass. spanClass é um número inteiro do tipo uint8. Os sete bits mais altos armazenam o valor class indicando 0-67. O último bit é usado para indicar noscan, ou seja, se contém ponteiros.

go
type spanClass uint8

func (sc spanClass) sizeclass() int8 {
  return int8(sc >> 1)
}

func (sc spanClass) noscan() bool {
  return sc&1 != 0
}

Ele tem um total de 68 valores diferentes. Todos os valores são armazenados no arquivo runtime.sizeclasses.go na forma de tabela. Em tempo de execução, usa-se spanClass através de runtime.class_to_size para obter o tamanho do objeto de mspan, e através de class_to_allocnpages para obter o número de páginas de mspan.

classtamanho máximo do objetotamanho do spanquantidade de objetosdesperdício no finaltaxa máxima de desperdício de memóriaalinhamento mínimo
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

A lógica de cálculo destes valores pode ser encontrada na função printComment em runtime.mksizeclasses.go. A fórmula de cálculo da taxa máxima de desperdício de memória é:

go
float64((size-prevSize-1)*objects+tailWaste) / float64(spanSize)

Por exemplo, quando class é 2, sua taxa máxima de desperdício de memória é:

((16-8-1)*512+0)/8192 = 0.4375

Quando o valor de class é 0, é o spanClass usado especificamente para alocar objetos grandes acima de 32KB. Basicamente, um objeto grande ocupa um mspan. Portanto, o heap do Go é na verdade composto por várias listas encadeadas mspan de tamanhos fixos diferentes.

heaparena

Como mencionado anteriormente, mspan é composto por várias páginas, mas mspan apenas mantém referência aos endereços das páginas, não负责 gerenciar estas páginas. Quem realmente负责 gerenciar estas páginas de memória é runtime.heaparena. Cada heaparena gerencia várias páginas. O tamanho de heaparena é determinado por runtime.heapArenaBytes, normalmente 64MB. bitmap é usado para identificar se o endereço correspondente na página contém objetos. zeroedBase é o endereço inicial da página de memória gerenciada por este heaparena. spans registra qual mspan está usando 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
}

A lógica relacionada ao registro de páginas e mspan pode ser encontrada no método mheap.setSpans, conforme mostrado abaixo:

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
  }
}

No heap do Go, é um array bidimensional de heaparena que gerencia todas as páginas de memória. Veja o campo mheap.arenas:

go
type mheap struct {
  arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
}

Na plataforma Windows de 64 bits, a primeira dimensão do array é 1 << 6, a segunda dimensão é 1 << 16. Na plataforma Linux de 64 bits, a primeira dimensão é 1, a segunda dimensão é 1 << 22. Este array bidimensional composto por todos os heaparena constitui o espaço de memória virtual em tempo de execução do Go. A visão geral é conforme mostrado no diagrama abaixo:

Embora os heaparena sejam adjacentes, as páginas de memória que eles gerenciam não são contíguas entre si.

mcache

mcache corresponde à estrutura runtime.mcache, que já apareceu no artigo de escalonamento concorrente. Embora seu nome seja mcache, na verdade está vinculado ao processador P. mcache é o cache de memória de cada processador P, que inclui o array de lista encadeada mspan alloc. O tamanho do array é fixo em 136, exatamente o dobro da quantidade de spanClass. Também inclui o cache de micro-objetos tiny, onde tiny aponta para o endereço inicial da memória de micro-objetos, tinyoffset é o offset da memória ociosa em relação ao endereço inicial, e tinyAllocs indica quantos micro-objetos foram alocados. Sobre o cache de stack stackcache, pode-se consultar Alocação de Memória de Stack.

go
type mcache struct {
    _ sys.NotInHeap

    nextSample uintptr // aciona amostra de heap após alocar esta quantidade de bytes
    scanAlloc  uintptr // bytes de heap escaneável alocados
    tiny       uintptr
    tinyoffset uintptr
    tinyAllocs uintptr

    alloc [numSpanClasses]*mspan
    stackcache [_NumStackOrders]stackfreelist
    flushGen atomic.Uint32
}

Durante inicialização, as listas encadeadas em alloc de mcache contêm apenas um nó cabeça vazio runtime.emptymspan, ou seja, mspan sem memória disponível.

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
}

Apenas quando é necessário realizar alocação de memória, solicita-se um novo mspan de mcentral para substituir o span vazio original. Este trabalho é completado pelo método mcache.refill. Sua única entrada de chamada é a função runtime.mallocgc. Abaixo está o código simplificado:

go
func (c *mcache) refill(spc spanClass) {
  // Retorna o span em cache atual para as listas centrais
  s := c.alloc[spc]

  // Obtém um novo span em cache das listas centrais
  s = mheap_.central[spc].mcentral.cacheSpan()
  if s == nil {
    throw("out of memory")
  }

  c.scanAlloc = 0

  c.alloc[spc] = s
}

A vantagem de usar mcache é que não é necessário lock global durante alocação de memória. Porém, quando sua memória é insuficiente, é necessário acessar mcentral, o que ainda requer lock.

mcentral

runtime.mcentral gerencia todos os mspan no heap que armazenam objetos pequenos. Ao solicitar memória em mcache, é mcentral que realiza a alocação.

go
type mcentral struct {
    _         sys.NotInHeap
    spanclass spanClass
    partial [2]spanSet
    full    [2]spanSet
}

Os campos de mcentral são poucos: spanClass indica o tipo de mspan armazenado; partial e full são dois spanSet, o primeiro armazena mspan com memória ociosa, o segundo armazena mspan sem memória ociosa. mcentral é gerenciado diretamente pelo heap mheap. Em tempo de execução, existem um 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 é principalmente responsável por dois trabalhos: quando há memória suficiente, aloca mspan disponível para mcache; quando a memória é insuficiente, solicita de mheap a alocação de um novo mspan. O trabalho de alocação de mspan para mcache é completado pelo método mcentral.cacheSpan. Primeiro, busca-se mspan disponível no conjunto varrido da lista ociosa:

go
// Tenta spans parciais varridos primeiro
sg := mheap_.sweepgen
if s = c.partialSwept(sg).pop(); s != nil {
    goto havespan
}

Se não encontrar, busca-se mspan disponível no conjunto não varrido da lista ociosa:

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
    }
}

Se ainda não encontrar, busca-se no conjunto não varrido da lista não ociosa:

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)
    }
}

Se finalmente ainda não encontrar, o método mcentral.grow solicita de mheap a alocação de um novo mspan:

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

Em condições normais, de qualquer forma será retornado um mspan disponível:

go
havespan:
  freeByteBase := s.freeindex &^ (64 - 1)
  whichByte := freeByteBase / 8
  // Inicializa cache de bits de alocação
  s.refillAllocCache(whichByte)
  s.allocCache >>= s.freeindex % 64

  return s

Para o processo de solicitar mspan de mheap, na verdade é chamada o método mheap.alloc, que retorna um novo 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
}

Após inicializá-lo adequadamente, pode ser alocado para mcache usar.

mheap

runtime.mheap é o gerenciador da memória heap da linguagem Go. Em tempo de execução, existe como variável global runtime.mheap_:

go
var mheap_ mheap

Ele gerencia todos os mspan criados, todos os mcentral, todos os heaparena, e muitos outros alocadores variados. Sua estrutura simplificada é conforme abaixo:

go
type mheap struct {
    _ sys.NotInHeap

    lock mutex

    allspans []*mspan // todos os spans lá fora

    pagesInUse         atomic.Uintptr // páginas de spans em stats mSpanInUse
    pagesSwept         atomic.Uint64  // páginas varridas neste ciclo
    pagesSweptBasis    atomic.Uint64  // pagesSwept para usar como origem da razão de varredura

    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 // estrutura de dados de alocação de páginas
    spanalloc              fixalloc // alocador para span*
    cachealloc             fixalloc // alocador para mcache*
    specialfinalizeralloc  fixalloc // alocador para specialfinalizer*
    specialprofilealloc    fixalloc // alocador para specialprofile*
    specialReachableAlloc  fixalloc // alocador para specialReachable
    specialPinCounterAlloc fixalloc // alocador para specialPinCounter
    arenaHintAlloc         fixalloc // alocador para arenaHints
}

Para mheap, em tempo de execução há principalmente quatro trabalhos a serem realizados:

  • Inicializar heap
  • Alocar mspan
  • Liberar mspan
  • Expansão de heap

Abaixo vamos abordar estas quatro coisas em ordem.

Inicialização

O período de inicialização do heap está localizado na fase de bootstrap do programa, que também é a fase de inicialização do escalonador. A ordem de chamada é:

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

Durante o período de inicialização, é principalmente responsável por executar o trabalho de inicialização de cada alocador:

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)
}

Isso inclui o alocador mheap.spanalloc负责分配 mspan e o alocador mheap.pages负责分配 páginas, bem como a inicialização de todos os mcentral.

Alocação

Em mheap, a alocação de mspan é completada pelo método mheap.allocSpan:

go
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan)

Se a memória solicitada for suficientemente pequena, ou seja, satisfaz npages < pageCachePages/4, tenta-se sem lock obter um mspan disponível no cache mspan do P local. Se o cache do P estiver vazio, também será inicializado primeiro:

go
// Se o cache estiver vazio, reabasteça-o
if c.empty() {
    lock(&h.lock)
    *c = h.pages.allocToCache()
    unlock(&h.lock)
}

Depois obtém-se do cache do P, completado pelo 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
        }
    }
}

O código para obter mspan do cache do P é conforme abaixo. Ele tenta obter o último mspan no cache:

go
func (h *mheap) tryAllocMSpan() *mspan {
  pp := getg().m.p.ptr()
  // Se não temos um p ou o cache está vazio, não podemos fazer
  // nada aqui
  if pp == nil || pp.mspancache.len == 0 {
    return nil
  }
  // Retira a última entrada no cache
  s := pp.mspancache.buf[pp.mspancache.len-1]
  pp.mspancache.len--
  return s
}

Se a memória solicitada for relativamente grande, será alocada no heap. Durante este processo é necessário manter lock:

go
lock(&h.lock)
if base == 0 {
    // Tenta adquirir um endereço base
    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 {
    // Falhamos em obter um mspan anteriormente, então pegamos
    // um agora que temos o lock do heap
    s = h.allocMSpanLocked()
}
unlock(&h.lock)

Primeiro usa-se pageAlloc.alloc para alocar memória de página suficiente. Se a memória do heap não for suficiente, mheap.grow realiza expansão. Após conclusão da alocação de memória de página, aloca-se 64 mspan do alocador em cadeia mheap.spanalloc para o cache local do P (64 é exatamente metade do comprimento do array de cache). Depois retorna-se um mspan disponível do cache do P:

go
func (h *mheap) allocMSpanLocked() *mspan {
  assertLockHeld(&h.lock)

  pp := getg().m.p.ptr()
  if pp == nil {
    // Não temos um p então apenas fazemos o normal
    return (*mspan)(h.spanalloc.alloc())
  }
  // Reabastece o cache se necessário
  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
  }
  // Retira a última entrada no cache
  s := pp.mspancache.buf[pp.mspancache.len-1]
  pp.mspancache.len--
  return s
}

De acordo com as duas situações acima, finalmente pode-se obter um mspan disponível. Finalmente, após inicializar mspan adequadamente, pode-se retornar:

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

Liberação

Como mspan é alocado pelo alocador em cadeia, naturalmente ao liberar memória também é ele que realiza a liberação:

go
func (h *mheap) freeSpanLocked(s *mspan, typ spanAllocType) {
  assertLockHeld(&h.lock)
  // Marca o espaço como livre
  h.pages.free(s.base(), s.npages)
  s.state.set(mSpanDead)
  h.freeMSpanLocked(s)
}

Primeiro marca-se através do alocador de páginas mheap.pages que a página de memória especificada foi liberada. Depois define-se o estado de mspan como mSpanDead. Finalmente, o alocador mheap.spanalloc libera mspan:

go
func (h *mheap) freeMSpanLocked(s *mspan) {
  assertLockHeld(&h.lock)

  pp := getg().m.p.ptr()
  // Primeiro tenta liberar o mspan diretamente para o cache
  if pp != nil && pp.mspancache.len < len(pp.mspancache.buf) {
    pp.mspancache.buf[pp.mspancache.len] = s
    pp.mspancache.len++
    return
  }
  // Falhando nisso (ou se não temos um p), apenas libera
  // de volta para a memória heap
  h.spanalloc.free(unsafe.Pointer(s))
}

Se o cache do P não estiver cheio, será colocado no cache local do P para continuar uso. Caso contrário, será liberado de volta para a memória heap.

Expansão

O espaço de memória de página gerenciado por heaparena não é todo solicitado antecipadamente no início. Apenas quando é necessário usar memória é que é alocado. Quem负责 expansão de memória heap é o método mheap.grow. Abaixo está o 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 {
      // Muda para o novo espaço
      h.curArena.base = uintptr(av)
      h.curArena.end = uintptr(av) + asize
    }
    nBase = alignUp(h.curArena.base+ask, physPageSize)
  }
  ...
}

Primeiro calcula-se a memória necessária de acordo com npage e realiza-se alinhamento. Depois julga-se se o heaparena atual tem memória suficiente. Se não tiver, mheap.sysAlloc solicita mais memória para o heaparena atual ou aloca um novo 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
    }
  }
    ...
}

Primeiro tenta-se usar o alocador linear mheap.arena para solicitar um bloco de memória no espaço de memória pré-alocado. Se falhar, realiza-se expansão de acordo com hintList. hintList é do tipo runtime.arenaHint, que registra especificamente informações de endereço relacionadas à expansão 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))
}

Após solicitação de memória, atualiza-se para o 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, o alocador de páginas marca esta memória como estado pronto:

go
// Atualiza estruturas do alocador de páginas para tornar este
// espaço pronto para alocação
h.pages.grow(v, nBase-v)
totalGrowth += nBase - v

Alocação de Objetos

Em Go, ao alocar memória para objetos, divide-se em três tipos diferentes de acordo com o tamanho:

  • Micro-objetos (tiny): menor que 16B
  • Pequenos objetos (small): menor que 32KB
  • Grandes objetos (large): maior que 32KB

De acordo com os três tipos diferentes, executa-se lógica diferente durante alocação de memória. A função负责分配 memória para objetos é runtime.mallocgc. Sua assinatura de função é conforme abaixo:

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

Ela tem apenas três parâmetros: tamanho da memória, tipo, e um valor booleano用于表示 se é necessário limpar memória. É a função de entrada para toda alocação de memória de objetos em Go. Ao usar a função new para criar ponteiros, também se entra nesta função. Quando a alocação de memória é bem-sucedida, o ponteiro retornado é o endereço do objeto. Na parte mspan mencionamos que cada mspan possui um spanClass. spanClass determina o tamanho fixo de mspan. Go divide objetos no intervalo [0, 32KB] em 68 tamanhos diferentes. Portanto, a memória do Go é composta por várias listas encadeadas mspan de tamanhos fixos diferentes. Ao alocar memória de objeto, basta calcular o spanClass correspondente de acordo com o tamanho do objeto, depois encontrar a lista encadeada mspan correspondente de acordo com spanClass, e finalmente buscar mspan disponível na lista encadeada. Esta prática hierárquica pode resolver de forma mais eficaz o problema de fragmentação de memória.

Micro-objetos

Todos os micro-objetos não-ponteiro menores que 16B são alocados pelo micro-alocador em P para o mesmo bloco de memória contíguo. Em runtime.mcache, o campo tiny registra o endereço base deste bloco de memória:

go
type mcache struct {
  tiny       uintptr
  tinyoffset uintptr
  tinyAllocs uintptr
}

O tamanho dos micro-objetos é determinado pela constante runtime.maxTinySize, que é 16B. O bloco de memória用于存储 micro-objetos também é deste tamanho. Geralmente, os objetos armazenados aqui são algumas pequenas strings. A parte do código负责分配 micro-objetos é conforme abaixo:

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
      }

      // Aloca um novo bloco maxTinySize
      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

Se o bloco de micro-memória atual tiver espaço suficiente para acomodar, usa-se diretamente o bloco de memória atual, ou seja, off+size <= maxTinySize. Se não for suficiente, primeiro tenta-se buscar espaço disponível no cache span de mcache. Se também não for possível, solicita-se um mspan de mcentral. De qualquer forma, finalmente se obtém um endereço disponível. Por fim, substitui-se o bloco de memória de micro-objeto antigo pelo novo.

Pequenos Objetos

A maioria dos objetos em tempo de execução da linguagem Go são pequenos objetos no intervalo [16B, 32KB]. O processo de alocação de pequenos objetos é o mais complicado, mas o código é o menor. A parte do código负责分配 pequenos objetos é conforme abaixo:

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)
}

Primeiro calcula-se qual spanClass deve ser usado de acordo com o tamanho do objeto. Depois, runtime.nextFreeFast tenta obter espaço de memória disponível no mspan em cache correspondente em mcache de acordo com spanClass:

go
func nextFreeFast(s *mspan) gclinkptr {
  theBit := sys.TrailingZeros64(s.allocCache) // Há um objeto livre no 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
}

A função de mspan.allocCache é registrar se há objetos usando espaço de memória. Ele divide memória um por um de acordo com a quantidade de objetos, não de acordo com o tamanho do espaço. Isso é equivalente a tratar mspan como um array de objetos, conforme mostrado no diagrama abaixo:

allocCache é um número de 64 bits. Cada bit corresponde a um bloco de espaço de memória. Se um determinado bit for 0, indica que há objeto usando. Se for 1, indica que este bloco de memória está ocioso. O objetivo de sys.TrailingZeros64(s.allocCache) é calcular a quantidade de zeros à direita. Se o resultado for 64, indica que não há memória ociosa disponível. Se houver, calcula-se o offset da memória ociosa e soma-se ao endereço base de mspan e retorna-se.

Quando mcache não tem espaço suficiente, solicita-se de mcentral. Este trabalho é completado pelo 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
}

O mcache.refill nele负责 solicitar um mspan disponível de mcentral:

go
func (c *mcache) refill(spc spanClass) {
  ...
  s = mheap_.central[spc].mcentral.cacheSpan()
  ...
}

O método mcentral.cacheSpan, quando a memória é insuficiente, realiza expansão através de mcentral.grow. A expansão solicita novo mspan de mheap:

go
func (c *mcentral) grow() *mspan {
  ...
  s := mheap_.alloc(npages, c.spanclass)
  ...
  return s
}

Portanto, finalmente, a alocação de memória de pequenos objetos é feita nível por nível: primeiro mcache, depois mcentral, e finalmente mheap. O custo de alocação de mcache é o mais baixo, pois é cache local do P, não requerendo lock durante alocação de memória. mcentral é o próximo. Solicitar memória diretamente de mheap tem o custo mais alto, pois o método mheap.alloc compete pelo lock global inteiro do heap.

Grandes Objetos

A alocação de grandes objetos é a mais simples. Se o tamanho do objeto exceder 32KB, solicita-se diretamente de mheap a alocação de um novo mspan para acomodar. A parte do código负责分配 grandes objetos é conforme abaixo:

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)
    }
}

Onde mcache.allocLarge负责 solicitar memória de grandes objetos de mheap:

go
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
  ...
  spc := makeSpanClass(0, noscan)
  s := mheap_.alloc(npages, spc)
  ...
  return s
}

Pode-se ver no código que o valor de spanClass usado por grandes objetos é 0. Grandes objetos basicamente ocupam um mspan por objeto.

Outros

Estatísticas de Memória

O runtime do Go expõe uma função ReadMemStats para usuários, que pode ser usada para estatísticas de memória do runtime:

go
func ReadMemStats(m *MemStats) {
  _ = m.Alloc // teste de verificação de nil antes de trocarmos stacks, veja issue 61158
  stopTheWorld(stwReadMemStats)

  systemstack(func() {
    readmemstats_m(m)
  })

  startTheWorld()
}

Mas o custo de usá-la é muito alto. Pode-se ver no código que é necessário STW antes de analisar a situação da memória. A duração do STW pode variar de alguns milissegundos a centenas de milissegundos. Geralmente é usada apenas durante depuração e solução de problemas. A estrutura runtime.MemStats registra informações relacionadas à memória heap, memória stack e GC:

go
type MemStats struct {
    //  Estatísticas gerais
    Alloc uint64
    TotalAlloc uint64
    Sys uint64
    Lookups uint64
    Mallocs uint64
    Frees uint64

    // Estatísticas de memória heap
    HeapAlloc uint64
    HeapSys uint64
    HeapIdle uint64
    HeapInuse uint64
    HeapReleased uint64
    HeapObjects uint64

    // Estatísticas de memória stack
    StackInuse uint64
    StackSys uint64

    // Estatísticas de componentes de memória
    MSpanInuse uint64
    MSpanSys uint64
    MCacheInuse uint64
    MCacheSys uint64
    BuckHashSys uint64

    // Estatísticas relacionadas ao 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

O alocador de memória é obviamente usado para alocar memória heap, mas o heap é dividido em duas partes: uma parte é a memória heap necessária para o próprio runtime do Go, e a outra parte é a memória heap disponível para usuários. Portanto, em algumas estruturas pode-se ver tal campo embutido:

go
_ sys.NotInHeap

Indicando que a memória deste tipo não será alocada no heap do usuário. Este tipo de campo embutido é especialmente comum em componentes de alocação de memória, como a estrutura runtime.mheap que representa o heap do usuário:

go
type mheap struct {
  _ sys.NotInHeap
}

O verdadeiro propósito de sys.NotInHeap é evitar barreiras de memória para melhorar a eficiência do runtime. O heap do usuário precisa executar GC, então requer barreiras de memória.

Golang por www.golangdev.cn edit