👋

Golangとメモリ管理について理解する

2022/12/20に公開

概要

GolangはC言語と近しい機能性を持ちながらも、コーディングしやすいということがよく言われます。
その要因の一つにメモリ管理があると思われます。
というのはメモリの割り当てが自動化されている点とガベージコレクタがあるからです

普段全くメモリ管理に気を使わずに来てしまったために、
今回はGolangのメモリ管理方法を観点として周辺知識を蓄えることを目的とします

メモリ確保

プログラム実行時に、必要に応じた動的に使えるメモリ領域として、2種類あります。
ヒープスタックです。

ヒープ

C言語でヒープを確保するために使う標準関数としてmalloc()があります。
C言語の授業で触れたのを覚えています。
こういうのですね。

int *addr;
addr = (int*)malloc(4);

必要容量を指定することでメモリブロックを確保して、ポインタを返します。

Golangではmallocのお仲間のTCMalloc(Google)が採用されています。
以下の特徴があります。

32KB以下の小さなオブジェクトはスレッドごとにメモリブロックを管理する
32KBより大きなオブジェクトは4KB単位に丸め、共有の領域で管理してメモリの無駄を減らす

スタック

関数呼び出し時、領域を確保して、関数を抜け出すときに破棄します。
まさにデータ構造としてのスタックを使うことが適しています。

確保される作業メモリ領域はスタックフレームと呼ばれます。
goroutineでは、4キロバイトサイズのスタックを確保します。
Linuxのデフォルトサイズは8MBなようなので、これに比べてかなり小さいですね。小さいため、起動も高速になります。

もし大きいスタックフレームが必要になったとしても
新しいスタックフレームを確保し,呼び出し元からの引数をコピーし,安全に実行できるようになった元の関数に制御を戻します。その関数が終了すると、処理は取り消され、その戻り引数は呼び出し元のスタックフレームにコピーバックされ、不要なスタックスペースは解放されます[1]

golangではどのように割り当てているか?

最初の話に戻ります。
C++ではローカル変数を宣言するとスタックにメモリが確保され、mallocを使うとヒープメモリにメモリが確保されます。

一方、Golangではその内部でメモリの自動割り当てをおこなってくれます。
ローカル変数として宣言しても、そのポインタが他の関数に渡ったり、返り値となるなど、変数の寿命が長くなる可能性があれば、ヒープに確保されます。

内部構造としては、デフォルトでは高速のためスタックを選択しようとします。
そこで、外部の関数や返り値に渡すと、ヒープに逃がします。

例えば、以下をビルドします。

package main

import "fmt"

func main() {
	a := [4]int{1, 2, 3, 4}
	fmt.Println(a)
}

Golangでは、-gcflags -mを渡すことで、メモリがスタックとヒープどちらに確保されているかを知ることができます。

go build -gcflags -m main.go
# command-line-arguments
./main.go:9:13: inlining call to fmt.Println
./main.go:9:13: ... argument does not escape
./main.go:9:13: a escapes to heap

a escapes to heapでヒープに置かれていることがわかります。
表示の参照関数であってもヒープに移動されてしまうようです。

ガベージコレクタ(GC)

割り当ての後は解放です。
ガベージのコレクタなので、まさにごメモリのごみ収集です。

解放されないままで放置されたヒープ領域が発生する問題を、メモリ・リークと呼びます。
この未解放のヒープ領域を回収する仕組みがガベージコレクションです。

多くの言語ではマークアンドスイープアルゴリズムが使われているようで、Golangでも採用されています。

マークアンドスイープ

  1. メモリ領域から不必要なデータにマーク付けする
  2. 不要なデータを削除する

アルゴリズムの基本的な方針としてはシンプルですが、
難点としては不要メモリを削除するときにプログラムを停止する必要があるところです。
これをStop The World(STW)と言います。
GCの研究では、STWによる停止時間を減らすことは、分野として大きく、
Golangではメモリのコストが高くつくため、この停止時間を減らす改良が行われています。

最後に

Golangでは技術者がメモリの動的確保と解放を気にする必要がありません。
しかし、内部の仕組みを知ってみると、中身では想像が及ばない工夫がされていることがわかります。
少しずつ中身の仕組みを知っていくことで、普段の開発にも活かしていきたいと思います。
スライスのメモリ確保の部分なども追加したいです。

参考

https://deeeet.com/writing/2016/05/08/gogc-2016/

https://it-trend.jp/development_tools/article/32-0041#chapter-2

脚注
  1. https://dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite ↩︎

Discussion