Goのガーベジコレクションってどんな感じ?
0. はじめに
たまたま Go のガーベジコレクションについて話す機会があったので公式ドキュメントを調べてみました。
普段の開発業務でメモリリークしないように意識してコーディングしている人は少ないと思いますが、調べてみると面白かったのでぜひ最後までご覧ください。
1. そもそもガーベジコレクションとは
ガーベジコレクション(GC)は、プログラムが使用しなくなったメモリを自動的に回収して再利用可能にする仕組みです。
Python や Java と比較して、Go の GC は非常に高速です。そのため、高パフォーマンスが要求されるサーバーアプリケーションで大活躍します。
また、C や C++では、プログラマーが手動でメモリ管理を行う必要がありますが、Go ではガーベジコレクタが自動的にメモリを管理してくれます。
これにより、
- メモリリークの防止
- ダングリングポインタのバグ削減※
- 開発者がビジネスロジックに集中できる
といったメリットがあります。
※ダングリングポインタ:プログラムが解放済みまたは無効になったメモリ領域を指し示すポインタのこと。このポインタを間接参照すると、予期しない動作やクラッシュを引き起こす、Use-After-Free と呼ばれる脆弱性の原因となることがある
2. Go のガーベジコレクションの基本
Go はTracing Garbage Collectionという方式を採用しています。
GC は、プログラムのルート(グローバル変数、スタック上の変数など)からスタートし、ポインタを辿りながら「ライブオブジェクト」を見つけ出します。そして、見つからなかったオブジェクトを「死んでいる」と判断して回収します。
- ライブオブジェクト: プログラムから参照されている(使われている)メモリ
3. GC の動作メカニズム
マーク&スイープアルゴリズム
Go のガーベジコレクタはマーク&スイープという 2 段階のアルゴリズムで動作します:
- マークフェーズ: ルートから辿れるすべてのオブジェクトに「使用中」のマークをつける
- スイープフェーズ: マークがついていないオブジェクトのメモリを回収する
GC サイクルの 3 つの状態
GC は以下の 3 つの状態を循環します:
| 状態 | 説明 |
|---|---|
| Off | GC が動作していない状態。アプリケーションが実行され、ヒープメモリが増加する |
| Marking | 生存オブジェクトを特定している状態。ポインタを辿ってマークをつける |
| Sweeping | マークされていないオブジェクトを回収し、メモリを再利用可能にする |
Go の GC における重要な特徴は、並行 GCを採用していることです。Marking フェーズの間もアプリケーションは動作し続けるため、長時間の停止(STW: Stop The World)が発生しにくい設計になっています。
4. GC のチューニングパラメータ
GoのGCをチューニングするにはGOGCというパラメータをいじります。
GOGCは、
- GC の実行頻度とピークメモリ使用量のバランスを調整するパラメータ
- デフォルト値は 100 が設定されている
- 前回の GC 後のヒープサイズを 100 としたときに、いくら増加したら GC を実行するか
# GOGC=100 (デフォルト)
# ヒープが2倍になったらGC実行
# 元が100なら、100+100=200 つまりは元の2倍
# GOGC=200
# ヒープが3倍になったらGC実行(GC頻度↓、ピークメモリ使用量↑)
# 元が100なら、100+200=200 つまりは元の3倍
# GOGC=50
# ヒープが1.5倍になったらGC実行(GC頻度↑、ピークメモリ使用量↓)
# 元が100なら、100+50=150 つまりは元の1.5倍
GOGC のチューニングには下記のようなトレードオフが発生する:
高い値(例: GOGC=200)
- GC 実行回数が減る → マーク&スイープ処理の CPU 時間が減少
- GC 実行までヒープが大きく成長 → 必要なメモリ容量が増加
低い値(例: GOGC=50)
- こまめに GC 実行 → ヒープメモリのピーク値を抑制
- GC 実行回数が増える → マーク&スイープ処理で CPU を頻繁に消費
+α GOMEMLIMIT
GOMEMLIMITは、Go プログラムが使えるメモリの上限を設定する環境変数です。
なぜ必要なのか?
GOGC だけでは、メモリが無限に増え続ける可能性があります:
例: GOGC=100、前回GC後のヒープ=100MB
→ 次のGCは200MBで実行
→ さらに次は400MBで実行
→ さらに次は800MBで実行...
サーバーのメモリが 2GB しかない場合、プログラムがクラッシュしてしまいます。
GOMEMLIMIT の役割
メモリの上限を設定することで、これ以上メモリを使えないという制限を GC に伝えます:
# 512MBの上限を設定
GOMEMLIMIT=512MiB go run main.go
この設定により:
- メモリ使用量が 512MB に近づくと、GC が積極的に動作
- GOGC の設定より優先してメモリを回収
- サーバーのメモリ不足を防ぐ
実際の使用例
Docker コンテナで動かす場合:
# コンテナに512MBのメモリを割り当て
docker run -m 512m myapp
# Goプログラムには450MBを上限として設定(余裕を持たせる)
GOMEMLIMIT=450MiB
GOGC との併用:
# 通常はGOGC=100で動作
# メモリが512MBに近づいたら強制的にGC実行
GOGC=100 GOMEMLIMIT=512MiB go run main.go
使いどころ
- コンテナ環境(Docker, Kubernetes)でメモリ制限がある
- クラウド環境で課金を抑えたい
- 複数のプロセスが同じサーバーで動いている
5. まとめ
ポイント:
- Go は Tracing Garbage Collection (マーク&スイープアルゴリズム)を採用
- GC サイクルは Off/Marking/Sweeping の 3 状態を循環
-
GOGCでメモリと CPU のバランスを調整 -
GOMEMLIMITでメモリ上限を設定 - 不要なヒープアロケーションを減らすことが最適化の基本
本当は Go のコードを実行しながら GC の流れを追うのが良いのですが、長くなったのでコード例は別の記事でやろうと思います。
Go のガーベジコレクションは、開発者がメモリ管理を意識せずにプログラミングできる強力な仕組みです。
多くの場合、Go のデフォルト設定で十分なパフォーマンスが得られます。パフォーマンス問題が発生した場合のみ、プロファイリングとチューニングを検討しましょう。
知らぬ間にこのようなすばらしい恩恵を受けているからこそ、日々の開発業務にフォーカスできているのだと実感しました。Go 言語に感謝です。
Discussion