ガベージコレクションについて知識がないので調べてみた。言語は普段使っているGo(Go歴半年ほど)にした。
GCとは
GC(ガベージコレクション)とは、*ヒープ領域の無駄なメモリを自動で解放する仕組み。
*コンパイルされたファイルをOSがプロセスを開始し実行する際に、ヒープ領域のメモリの割り当てが行われる。
GCの恩恵
GCがないとどうなるのか?
プログラマがメモリ解放の設計をミスするとメモリリークが発生して無駄なメモリが増える。
実行しているマシンのRAMに確保可能なメモリがなくなり、仮想メモリも使い果たすとメモリ確保の失敗を想定していないプログラムだと通常プロセスの異常終了を引き起こす。(Wikiより)
しかし、プロセスがすぐに終了する場合はプロセス終了後にメモリは開放されるので問題ないが以下の場合、事態は深刻である。
- プログラムが長時間動く場合(APIサーバ、組み込みのシステムなど)
- 共用メモリなど確保したまま終了することが許されるメモリ領域をプログラムが扱っている場合
各言語のGCの仕組み
以下は僕が今まで経験してきた言語のGCの一覧
Ruby
MRI(Matz’ Ruby Implementation)という言語の処理系のGCについて説明する。
MRIのGCはマーク・スイープというアルゴリズムが使われている。
マーク・スイープはあらかじめ用意されたメモリ領域(ヒープ)をオブジェクトに順に割り当てる。
ヒープが足りなくなったらプログラムを停止し、オブジェクトがまだ生存しているかをマークしていき、マークされていないオブジェクトを削除していく(これをスイープという)、という動きをする(DeveloperIOより)
Python
参照カウンタを主とし、世代別GCを補助として使用している。
参照カウンタはポインタを参照するごとにカウンタをインクリメントし、参照されなくなるとデクリメントする。カウンタが0になるとメモリを開放する。
世代別GCは、一時的に使うメモリオブジェクト(第一世代)と長期的に使うオブジェクト(第二世代)に分けて、メモリ管理を行う。第一世代では頻繁にコピーGCを繰り返し回収を行い、一定回数以上回収されなかったメモリオブジェクトは第二世代に移動する。
GoのGCはどうやって実行されるのか?
調べる前は、GoのGCが厄介だと思った。
なぜなら、Goroutineが存在するからだ。
GoはOSにあるプロセスを管理するSchedulerとは別にruntimeがスケジューラを持ち、Goroutineの実行時間を管理している。
どのタイミングでGCのコードが動くのか?
またSTW(Stop The World)のレイテンシはどれ程かかるのかなど様々な疑問が湧いた。
どうやらGo(1.4以前は単純なSTW)のGCではTri-coloer markingとConcurrent GCという方法を使うことでSTWを減らし、レイテンシを改善したらしい。(まだ改善の余地はあると思うが)
Tri-coloer marking
メモリオブジェクトを以下の3種類に分けることで、一度にGCのプロセスを実行するのではなくGCの実行を分割することでレイテンシを小さくする。
- 白: まだ探索されていないオブジェクト
- グレー: 探索途中のオブジェクト
- 黒: 探索済みのオブジェクト
詳細は下の記事を読んで欲しい。
Concurrent GC
GoのConcurrent GCの仕組みはSTWの回数を2回に減らしている。
以下はConcurrent CCのフローである。
①全てのGoroutineを止める(STW1)
②*ライトバリアをONにして、GC用のGoroutineを使用しMarkingを行う。(25%のCPUを占有する)。
③マークアシスタントを使い、別のP(Go Schedulerより)でGCの手伝いを行う。
④Markingが終了し、ライトバリアをOFFにする。(STW2)
*マーク漏れを防ぐための手法(新たに参照されるオブジェクトが白いオブジェクトであればそれをグレーとするなど)
以下に詳細がまとまっている。
まとめ
GoのGCはかなりわかりやすいと思う。
一方で、世代別GCのように世代間移動のしきい値や第一世代領域のサイズをチューニングすることはできない。
環境変数のGOGCの目標パーセンテージ??を設定できるらしい。デフォルトは100でfalseでGCを使わないこともできる。(STWがネックなら使わないのもありかもしれない)
次は、GoのruntimeのScheduler周りをまとめたい。