Skip to content

memory

従来の c/c++ とは異なり、go は gc 言語で、ほとんどの場合メモリの割り当てと破棄は go によって自動的に管理されます。オブジェクトのメモリがスタック上に割り当てられるかヒープ上に割り当てられるかはコンパイラによって決定され、基本的にユーザーはメモリ管理に参加する必要はなく、ユーザーが行うのはメモリを使用することだけです。go におけるヒープメモリ管理には主に 2 つの大きなコンポーネントがあります。メモリアロケータはヒープメモリの割り当てを担当し、ガベージコレクターは無用なヒープメモリを回収して解放する責任を負います。本記事では主にメモリアロケータの作業方式について説明します。go メモリアロケータはグーグルの TCMalloc メモリアロケータの影響を大きく受けています。

アロケータ

go には 2 種類のメモリアロケータがあります。1 つは線形アロケータで、もう 1 つはチェーンアロケータです。

線形割り当て

線形アロケータは runtime.linearAlloc 構造体に対応し、以下の通りです。

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
}

このアロケータはオペレーティングシステムに連続したメモリスペースを事前に申請し、next は使用可能なメモリアドレスを指し、end はメモリスペースの末尾アドレスを指します。概ね以下の図のように理解できます。

線形アロケータのメモリ割り当て方式は非常に理解しやすく、申請するメモリサイズに基づいて十分な剩余スペースがあるかどうかをチェックします。十分な場合は next フィールドを更新して剩余スペースの開始アドレスを返します。コードは以下の通りです。

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

この割り当て方式の利点は高速でシンプルであることで、欠点も非常に明確です。解放されたメモリを再利用できないことです。next フィールドは剩余のスペースメモリアドレスのみを指し、以前使用されて解放されたメモリスペースを感知できないため、これにより大きなメモリスペースの浪費が発生します。以下の図の通りです。

したがって線形割り当ては go における主要な割り当て方式ではなく、32 ビットマシンでのみメモリ事前割り当て機能として使用されます。

チェーン割り当て

チェーンアロケータは構造体 runtime.fixalloc に対応し、チェーンアロケータによって割り当てられるメモリは連続しておらず、単方向リンクドリストの形式で存在します。チェーンアロケータは若干の固定サイズのメモリブロックで構成され、各メモリブロックは若干の固定サイズのメモリフラグメントで構成されます。メモリ割り当てを行うたびに、固定サイズのメモリフラグメントが使用されます。

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
}

そのフィールドは線形アロケータほどシンプルで理解しやすいものではありません。ここでは重要なものを簡単に紹介します。

  • size、メモリ割り当て時に毎回使用するメモリ量を指します。
  • list、再利用可能なメモリフラグメントのヘッダーノードを指し、各メモリスペースのサイズは size によって決定されます。
  • chunk、現在使用されているメモリブロック内のアイドルアドレスを指します
  • nchunk、現在のメモリブロックの剩余利用可能バイト数
  • nalloc、メモリブロックのサイズで、固定で 16KB です。
  • inuse、总共に使用されたバイト数
  • zero、メモリブロックを再利用する際にメモリをクリアするかどうか

チェーンアロケータは現在のメモリブロックと再利用可能なメモリフラグメントの参照を保持しています。各メモリブロックのサイズは固定で 16KB で、この値は初期化時に設定されます。

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
}

メモリブロックの分布は以下の図の通りで、図中のメモリブロックは作成時間の先後によって並べられていますが、実際にはそれらのアドレスは連続していません。

チェーンアロケータによって毎回割り当てられるメモリサイズも固定で、fixalloc.size によって決定されます。割り当て時にはまず再利用可能なメモリブロックがあるかどうかをチェックし、ある場合は優先的に再利用メモリブロックを使用し、その後現在のメモリブロックを使用します。現在のメモリブロックの剩余スペースが容纳できない場合は新しいメモリブロックを作成します。この部分のロジックは以下のコードに対応します。

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
}

チェーンアロケータの利点は解放されたメモリを再利用できることです。メモリを再利用する基本単位は固定サイズのメモリフラグメントで、そのサイズは fixalloc.size によって決定されます。メモリを解放する際、チェーンアロケータはそのメモリフラグメントをヘッダーノードとしてアイドルメモリフラグメントリンクドリストに追加します。コードは以下の通りです。

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

メモリコンポーネント

go におけるメモリアロケータは主に mspanheaparenamcachemcentralmheap のいくつかのコンポーネントで構成され、それらは階層的に作用し、go のヒープメモリ全体を管理しています。

mspan

runtime.mspan は go メモリ割り当ての基本的な単位で、その構造は以下の通りです。

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

mspanmspan の間は nextprev によって双方向リンクドリストの形式でリンクされ、メモリアドレスは連続していません。各 mspanmspan.npages 個の runtime.pageSize サイズのページメモリを管理し、通常ページサイズは 8KB で、mspan.startAddr がこれらのページの開始アドレスを記録し、mspan.limit が使用済みメモリの末端アドレスを記録します。各 mspan が格納する要素サイズ elemsize は固定であるため、容纳できる要素数も固定です。数が固定されているため、オブジェクトの格納は配列のように mspan 内に分布し、範囲は [0, nelems] で、freeindex が次にオブジェクトを格納するために使用できるインデックスを記録します。mspan には合計 3 つの状態があります。

  • mSpanDead、メモリはすでに解放されています
  • mSpanInUse、ヒープ上に割り当てられています
  • mSpanManual、手動管理メモリ用に割り当てられています。例えばスタックなど。

mspan の要素サイズを決定するのは spanClass で、spanClass 自体は uint8 タイプの整数で、上位 7 ビットは 0-67 の class 値を格納し、最後のビットは noscan つまりポインタを含むかどうかを示すために使用されます。

go
type spanClass uint8

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

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

合計 68 種類の異なる値があり、すべての値はテーブル形式で runtime.sizeclasses.go ファイルに格納されています。実行時、spanClass を使用して runtime.class_to_size によって mspan のオブジェクトサイズを取得でき、class_to_allocnpages によって mspan のページ数を取得できます。

class最大オブジェクトサイズspan サイズオブジェクト数末尾の浪費最大メモリ浪費率最小アライメント
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

これらの値の計算ロジックは runtime.mksizeclasses.goprintComment 関数で見つけることができ、その最大メモリ浪費率の計算式は以下の通りです。

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

例えば、class が 2 の場合、その最大メモリ浪費率は以下の通りです。

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

class 値が 0 の場合、32KB 以上の大オブジェクトを割り当てるために専用に使用される spanClass で、基本的に 1 つの大オブジェクトが 1 つの mspan を占有します。したがって、go のヒープメモリは実際には若干の異なる固定サイズの mspan で構成されています。

heaparena

前述したように mspan は若干のページで構成されていますが、mspan はページのアドレス参照を保持しているだけで、これらのページを管理する責任は負いません。実際にこれらのページメモリを管理する責任を負うのは runtime.heaparena です。各 heaparena は若干のページを管理し、heaparena のサイズは runtime.heapArenaBytes によって決定され、通常は 64MB です。bitmap はページ内の対応するアドレスがオブジェクトを格納しているかどうかを識別するために使用され、zeroedBase はその heaparena が管理するページメモリの開始アドレスで、spans によって各ページがどの mspan によって使用されているかが記録されます。

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
}

ページと mspan 記録に関するロジックは mheap.setSpans メソッドで見つけることができ、以下の通りです。

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

go ヒープでは、2 次元の heaparena 配列によってすべてのページメモリが管理されています。mheap.arenas フィールドを参照してください。

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

64 ビット Windows プラットフォームでは、配列の 1 次元は 1 << 6、2 次元は 1 << 16 です。64 ビット Linux プラットフォームでは、1 次元は 1、2 次元は 1 << 22 です。すべての heaparena で構成されるこの 2 次元配列は go ランタイムの仮想メモリスペースを構成し、全体的に見ると以下の図の通りです。

heaparena 間は隣接していますが、それらが管理するページメモリ間は連続していません。

mcache

mcacheruntime.mcache 構造体に対応し、並行スケジューリングの文書ですでに言及されています。名前は mcache ですが、実際にはプロセッサ P にバインドされています。mcache は各プロセッサ P 上のメモリキャッシュで、mspan リンクドリスト配列 alloc が含まれています。配列のサイズは固定で 136 で、ちょうど spanClass 数の 2 倍です。またマイクロオブジェクトキャッシュ tiny も含まれています。tiny はマイクロオブジェクトメモリの開始アドレスを指し、tinyoffset はアイドルメモリが開始アドレスに対して相対的なオフセットで、tinyAllocs は何個のマイクロオブジェクトが割り当てられたかを示します。スタックキャッシュ stackcache については、スタックメモリ割り当て で了解できます。

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
}

初期化時、mcache 内の alloc 内のリンクドリストは空のヘッダーノード runtime.emptymspan のみを含んでいます。つまり利用可能なメモリのない mspan です。

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
}

メモリ割り当てが必要な場合にのみ、元の空の span を置き換えるために mcentral から新しい mspan を申請します。この部分の作業は mcache.refill メソッドによって完了され、その唯一の呼び出し入口は runtime.mallocgc 関数です。以下は簡略化後のコードです。

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
}

mcache を使用する利点は、メモリ割り当て時にグローバルロックが不要なことです。ただしメモリが不足した場合は mcentral にアクセスする必要があり、この時は依然としてロックが必要です。

mcentral

runtime.mcentral はヒープ内のすべての小オブジェクトを格納する mspan を管理し、mcache がメモリを申請する際も mcentral によって割り当てられます。

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

mcentral のフィールドは少なく、spanClass は格納される mspan タイプを示し、partialfull は 2 つの spanSet で、前者はアイドルメモリのある mspan を格納し、後者はアイドルメモリのない mspan を格納します。mcentralmheap ヒープによって直接管理され、実行時には合計 136 個の mcentral があります。

go
type mheap struct {
    central [numSpanClasses]struct {
        mcentral mcentral
        pad      [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
    }
}

mcentral は主に 2 つの作業を担当します。メモリが十分な場合に mcache に利用可能な mspan を割り当てることと、メモリが不足した場合に mheap に新しい mspan の割り当てを申請することです。mcachemspan を割り当てる作業は mcentral.cacheSpan メソッドによって完了されます。まずアイドルリストの清扫済み集合から利用可能な mspan を探します。

go
// Try partial swept spans first.
sg := mheap_.sweepgen
if s = c.partialSwept(sg).pop(); s != nil {
    goto havespan
}

見つからない場合は、アイドルリストの未清掃集合から利用可能な mspan を探します。

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

それでも見つからない場合は、非アイドルリストの未清掃集合を探します。

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

最終的にそれでも見つからない場合は、mcentral.grow メソッドによって mheap に新しい mspan の割り当てを申請します。

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

通常の状況では、どのようにしても利用可能な mspan が返されます。

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

  return s

mheapmspan を申請するプロセスは、実際には mheap.alloc メソッドを呼び出すことで、このメソッドは新しい 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
}

これを初期化した後、mcache に割り当てて使用できます。

mheap

runtime.mheap は go 言語ヒープメモリの管理者で、実行時にはグローバル変数 runtime.mheap_ として存在します。

go
var mheap_ mheap

これは作成されたすべての mspan、すべての mcentral、およびすべての heaparena、および多くの他のさまざまなアロケータを管理します。簡略化後の構造は以下の通りです。

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
}

mheap にとって、実行時には主に以下の 4 つの作業を行う必要があります。

  • ヒープの初期化
  • mspan の割り当て
  • mspan の解放
  • ヒープの拡張

以下では順序に従ってこれら 4 つの事柄について説明します。

初期化

ヒープの初期化時期はプログラムのガイド段階にあり、同時にスケジューラの初期化段階でもあります。呼び出し順序は以下の通りです。

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

初期化時期には、主に各アロケータの初期化作業を実行する責任を負います。

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

これには mspan を割り当てる責任を負うアロケータ mheap.spanalloc とページ割り当てを担当するアロケータ mheap.pages、およびすべての mcentral の初期化が含まれます。

割り当て

mheap において、mspan の割り当てはすべて mheap.allocSpan メソッドによって完了されます。

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

申請された割り当てメモリが十分に小さい場合、つまり npages < pageCachePages/4 を満たす場合、ロックなしでローカル P 内の mspan キャッシュから利用可能な mspan を取得しようとします。P のキャッシュが空の場合は、まず初期化を行います。

go
// If the cache is empty, refill it.
if c.empty() {
    lock(&h.lock)
    *c = h.pages.allocToCache()
    unlock(&h.lock)
}

その後 P キャッシュから取得し、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
        }
    }
}

P キャッシュから mspan を取得するコードは以下の通りで、キャッシュ内の最後の mspan を取得しようとします。

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
}

申請されたメモリが比較的大きい場合は、ヒープ上でメモリを割り当てます。このプロセス中はロックを保持する必要があります。

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)

まず pageAlloc.alloc を使用して十分なページメモリを割り当てます。ヒープメモリが不足している場合は mheap.grow によって拡張されます。ページメモリ割り当て完了後、チェーンアロケータ mheap.spanalloc によって 64 個の mspan が P ローカルキャッシュに割り当てられます。64 はちょうどキャッシュ配列長さの半分です。その後 P キャッシュから利用可能な mspan を返します。

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
}

上記の 2 つの状況に基づき、最終的に利用可能な mspan を取得できます。最後に mspan を初期化完了後に返すことができます。

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

解放

mspan がチェーンアロケータによって割り当てられているため、メモリを解放する際もそれによって解放されます。

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

まずページアロケータ mheap.pages によって指定されたページメモリが解放されたことをマークし、その後 mspan の状態を mSpanDead に設定し、最後に mheap.spanalloc アロケータによって 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))
}

P キャッシュが満杯でない場合は、P ローカルキャッシュに配置して継続して使用し、そうでない場合はヒープメモリに解放されます。

拡張

heaparena が管理するページメモリスペースは初期にすべて申請されたわけではなく、メモリが必要になったときにのみ割り当てられます。ヒープメモリを拡張する責任を負うのは mheap.grow メソッドで、以下は簡略化後のコードです。

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

まず npage に基づいて必要なメモリを計算してアライメントを行い、その後現在の heaparena に十分なメモリがあるかどうかを判断します。不足している場合は mheap.sysAlloc によって現在の heaparena にさらにメモリを申請するか、新しい 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
    }
  }
    ...
}

まず線形アロケータ mheap.arena を使用して事前割り当てされたメモリスペースからメモリを申請しようとします。失敗した場合は hintList に基づいて拡張を行います。hintList のタイプは runtime.arenaHint で、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))
}

メモリ申請完了後、それを arenas 2 次元配列に更新します。

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

最後にページアロケータによってこのメモリスペースを準備完了状態にマークします。

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

オブジェクト割り当て

go がオブジェクトにメモリを割り当てる際、サイズに基づいて 3 つの異なるタイプに分類されます。

  • マイクロオブジェクト - tiny、16B 未満
  • 小オブジェクト - small、32KB 未満
  • 大オブジェクト - large、32KB 超

3 つの異なるタイプに基づき、メモリを割り当てる際に異なるロジックが実行されます。オブジェクトにメモリを割り当てる責任を負う関数は runtime.mallocgc で、その関数シグネチャは以下の通りです。

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

これには 3 つのパラメータのみあります。メモリサイズ、タイプ、およびメモリをクリアする必要があるかどうかを示すブール値です。これはすべての go オブジェクトメモリ割り当ての入口関数で、通常 new 関数を使用してポインタを作成する際もこの関数に入ります。メモリ割り当て成功後、これが返すポインタはそのオブジェクトのアドレスになります。mspan 部分で言及したように、各 mspanspanClass を持っており、spanClassmspan の固定サイズを決定し、go はオブジェクトを [0, 32KB] の範囲で 68 種類の異なるサイズに分類しています。したがって go メモリは若干の異なる固定サイズの mspan リンクドリストで構成されています。オブジェクトメモリを割り当てる際、オブジェクトサイズに基づいて対応する spanClass を計算し、その後 spanClass に基づいて対応する mspan リンクドリストを見つけ、最後にリンクドリストから利用可能な mspan を探すだけです。この階層化された做法はメモリフラグメントの問題をより効果的に解決できます。

マイクロオブジェクト

16B 未満のすべての非ポインタマイクロオブジェクトは P 内のマイクロアロケータによって同じ連続メモリに割り当てられます。runtime.mcache 内では、tiny フィールドによってこのメモリのベースアドレスが記録されます。

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

マイクロオブジェクトのサイズは runtime.maxTinySize 定数によって決定され、すべて 16B で、マイクロオブジェクトを格納するために使用されるメモリブロックもこのサイズです。一般的にここに格納されるオブジェクトは一些小さな文字列などです。マイクロオブジェクトを割り当てる責任を負う部分のコードは以下の通りです。

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

現在のマイクロメモリブロックに容纳するための十分なスペースがある場合、つまり off+size <= maxTinySize の場合は、直接現在のメモリブロックを使用します。十分でない場合は、まず mcache の span キャッシュから利用可能なスペースを探そうとし、それでもダメな場合は mcentralmspan を申請します。どのようにしても最終的に利用可能なアドレスが取得でき、最後に新しいマイクロオブジェクトメモリブロックで古いものを置き換えます。

小オブジェクト

go 言語ランタイムの大部分のオブジェクトは [16B, 32KB] この範囲内の小オブジェクトです。小オブジェクトの割り当てプロセスが最も面倒ですが、コードは最も少ないです。小オブジェクト割り当てを担当する部分のコードは以下の通りです。

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

まずオブジェクトのサイズに基づいてどのクラスの spanClass を使用するべきかを計算し、その後 runtime.nextFreeFast によって spanClass に基づいて mcache 内の対応するキャッシュ mspan から利用可能なメモリスペースを取得しようとします。

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
}

mspan.allocCache の役割はメモリスペースにオブジェクトが使用されているかどうかを記録することで、サイズに基づいてメモリを划分するのではなくオブジェクト数に基づいてメモリを 1 つずつ划分します。これは mspan をオブジェクト配列と見なすようなもので、以下の図の通りです。

allocCache は 64 ビット数字で、各ビットは一片のメモリスペースに対応し、あるビットが 0 の場合はオブジェクトによって使用されていることを示し、1 の場合はこのメモリがアイドルであることを示します。sys.TrailingZeros64(s.allocCache) の目的は末尾ゼロの数を計算することで、結果が 64 の場合は使用可能なアイドルメモリがないことを示し、ある場合はアイドルメモリのオフセットを計算して mspan のベースアドレスに加えて返します。

mcache に十分なスペースがない場合は、mcentral に申請しに行きます。この部分の作業は 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.refillmcentral に利用可能な mspan を申請する責任を負います。

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

mcentral.cacheSpan メソッドはメモリが不足した際に mcentral.grow によって拡張が行われます。拡張はさらに mheap に新しい mspan を申請します。

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

したがって最終的に見ると、小オブジェクトのメモリ割り当ては階層的に下に向かって進むことで、まずは mcache、然后是 mcentral、最后是 mheapmcache 割り当てのコストは最も低く、それは P ローカルキャッシュであるため、メモリを割り当てる際にロックを保持する必要がありません。mcentral はそれより次で、直接 mheap にメモリを申請するコストが最も高く、mheap.alloc メソッドはヒープ全体のグローバルロックを競合するためです。

大オブジェクト

大オブジェクト割り当てが最もシンプルで、オブジェクトのサイズが 32KB を超えた場合は、直接 mheap に新しい mspan の割り当てを申請して容纳します。大オブジェクト割り当てを担当する部分のコードは以下の通りです。

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.allocLargemheap に大オブジェクトのメモリスペースを申請する責任を負います。

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

コードから見ると、大オブジェクトが使用する spanClass 値は 0 で、大オブジェクトは基本的に 1 つのオブジェクトが 1 つの mspan を占有します。

その他

メモリ統計

go ランタイムはユーザーに 1 つの関数 ReadMemStats を公開しており、ランタイムのメモリ状況を統計するために使用できます。

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

しかしそれを使用する代償は非常に大きく、コードから見るとメモリ状況を分析する前に STW が必要で、STW の長さは数ミリ秒から数百ミリ秒まで異なる可能性があり、一般的にはデバッグと問題調査時にのみ使用されます。runtime.MemStats 構造体はヒープメモリ、スタックメモリ、および 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

メモリアロケータは明らかにヒープメモリを割り当てるために使用されますが、ヒープは 2 つの部分に分類されます。1 つは go ランタイム自体に必要なヒープメモリで、もう 1 つはユーザーに開放されるヒープメモリです。したがって一部の構造ではこのような埋め込みフィールドが見られます。

go
_ sys.NotInHeap

これはそのタイプのメモリがユーザーヒープ上に割り当てられないことを示し、この埋め込みフィールドはメモリ割り当てコンポーネントで特に一般的です。例えばユーザーヒープを表す構造体 runtime.mheap などです。

go
type mheap struct {
  _ sys.NotInHeap
}

sys.NotInHeap の実際の役割はメモリバリアを回避してランタイム効率を向上させることで、ユーザーヒープは GC を実行する必要があるためメモリバリアが必要です。

Golang学习网由www.golangdev.cn整理维护