OS入門:プロセス・メモリ・ファイルシステム
ガベージコレクション概要
この回でやること
不要になったメモリをランタイムが自動解放するガベージコレクションの仕組みを、参照カウントやマークアンドスイープなど主要アルゴリズムの違いで解説します。
- 読む 約 15 分
解放を書き忘れた 1 行が、3 日後にサーバーを落とす
まず、変数と、その変数が指しているデータの実体は別物として考えます。この実体をオブジェクトと呼び、その置き場所を指す情報が参照で、C ではポインタを使って置き場所を扱います。たとえば Python で a = {} と書くと、辞書の実体が作られ、変数 a はそれを指します。b = a とすると、辞書のコピーが増えるのではなく、a と b が同じ辞書を指します。変数を使える範囲をスコープと呼び、関数の中だけで使う変数なら、ふつうは関数の実行が終わるとその変数の寿命も終わります。del a で変数 a を消しても、b が指している辞書の実体は残ります。辞書の中にも別のオブジェクトへの参照を入れられるので、変数を消すことと、実体への参照がすべてなくなることは同じではありません。なお、C の malloc は実体を置くメモリを確保し、free はその領域を空きに戻す操作です。
C で malloc した領域は、free を呼ぶまで空きに戻りません。1 リクエストにつき 200 バイト解放し忘れるだけでも、毎秒 100 リクエストを捌くサーバーなら 1 日で 1.7GB 積み上がります。逆に、まだ誰かが持っているポインタの先を free してしまうと、次にそこを読んだコードが意味不明な値をつかみます。前者がメモリリーク、後者が use-after-free です。
どちらも「もう誰も使っていないか」を人間が判断しているから起きます。ならばその判断を機械にやらせよう、というのがガベージコレクションです。
素朴な答えは、指されている数を数えること
一番わかりやすいのは、オブジェクトごとに「今いくつの変数から指されているか」を数える方法です。代入で 1 増え、変数がスコープを抜けると 1 減り、0 になった瞬間に解放します。CPython の主力はこれで、sys.getrefcount で数を覗けます。
不要になった瞬間に回収されるので、掃除のために止まる時間もほぼありません。ただしこの方式には、塞げない穴が 1 つあります。
Python
a = {}
b = {}
a["next"] = b
b["prev"] = a
del a, b # 変数は消えたが、互いに指し合っているのでカウントは 1 のまま外から見れば完全なゴミですが、2 つは互いを指しているためカウントが 0 になりません。輪になったゴミは、数を数える方式では永久に回収できません。
「指されているか」ではなく「辿り着けるか」で決める
そこで発想を変えます。実行中のスタックやグローバル変数を出発点として、参照をひたすら辿ります。辿り着けたものが生きているもので、それ以外はすべてゴミです。印を付けて回る工程をマーク、印の無いものを回収する工程をスイープと呼びます。
D と E は互いを指していますが、出発点から辿り着けないので回収されます。先ほどの穴が塞がりました。
代わりに支払うものがあります。辿っている最中にオブジェクトの繋がりが書き換わると答えが狂うため、この方式はアプリを止めて実行する必要が出てきます。数十ミリ秒アプリ全体が固まる、あの停止時間の正体です。Java の ZGC や Go のように、アプリと並行して辿ることで停止を 1 ミリ秒以下に抑える実装が主流になってきました。
GC があってもリークは起きる
回収されないのは「辿り着けてしまうゴミ」です。グローバルな辞書にキャッシュを入れっぱなしにする、イベントリスナーを登録したまま外し忘れる。参照が残っている以上、GC から見れば生きているオブジェクトなので、いつまでも解放されません。
自動になったのは解放の実行であって、「もう要らない」と手放す判断ではない、ということです。