ふと気になったのでガベージコレクション(GC)の代表的なアルゴリズムを要点だけ3行でまとめてみます。
内容は基本的に一般教養としてのGarbage Collectionを参考にしています。ありがとうございます。
GCとは何かについては省略します。参考文献を参照ください。
どこかから参照されているオブジェクトは使われている、という考え方のもとオブジェクトごとに参照されている数を保持しておきゼロになったら解放するという方式です。
利点としては次に紹介するTracing GCのように一気に解放ということをしないので、停止時間が短くなることです。
欠点としてはオブジェクトを参照するたびに余計な処理が増えるので実行時間が遅くなること、循環参照した場合にメモリリークが発生してしまうことです。
プログラムが直接触れるレジスタやスタックなどから参照されている(参照からさらに参照されているものも含む)オブジェクトのみ必要で、それ以外は使われていないという考え方です。 よって定期的にヒープ全体を見渡して、使われていないオブジェクトを解放するという方式になります。
利点は循環参照でも解放できること、欠点は停止時間が大きくなってしまうことです。
以下の2つがTracing GCを実装するメジャーなアルゴリズムです。
各オブジェクトに探索済みかどうかを示すマークビットを割り当て、0であれば未探索、1であれば探索済みとします。
最初は全て0なので、まずマークフェイズでレジスタなどから参照されているオブジェクトを再帰的に1にしていきます。 次にスイープフェイズでマークビットが0のオブジェクトを解放します。
ヒープを二分割しておいて、片方のエリアがいっぱいになったらまだ使っているオブジェクトをもう片方のエリアにコピーするという方式です。
実はReference CountingとMark-Sweepはどちらも空いているメモリがまばらになってしまうため連続した領域を確保しづらい(=フラグメンテーション)という問題がありました。 Copyingではコピーするときにもう片方のエリアにオブジェクトを連続して並べることで、フラグメンテーションが起きないという利点があります。
前節が大まかなアルゴリズムの紹介になりますが、これほどナイーブな実装をそのまま採用していることは今時ないと思います。
代表的な改良方法もいくつか併せて紹介します。
Mark-Sweepでは一気にマークフェイズとスイープフェイズを実行するために停止時間が大きくなってしまうという問題がありました。
Lazy Sweepだとヒープがいっぱいになったタイミングではマークフェイズだけを実行することにします。
そして実際にmalloc
要求が発生したタイミングでスイープフェイズを少しずつ実行して、空き領域をユーザープログラムに渡していきます。
こちらもTracing GCでの停止時間を短くするための方法です。
Incremental GCでは探索処理をまとめてやるのではなく、ユーザープログラムと交互にちょっとずつ実行することで停止時間を短くします。 しかしGCの最中にユーザープログラムによってオブジェクトが書き換えられてしまうため、write barrierというしくみが別途必要になります(詳細は参考元をご覧ください)。
こちらも停止時間を短くするための方法ですが、探索する範囲を狭めるというアプローチです。
Generational GCでは新しめのヒープと古めのヒープに分けてオブジェクトを管理し、新しく作成されたオブジェクトはまず新しめのヒープに確保されます。 新しめのヒープで何度かGCされても解放されなかったオブジェクトはこの次のGCでも生き残るはずだと考え、古めのヒープに移されます。 古めのヒープには新規オブジェクトは作成されず、いっぱいにならない限りGCされないので、結果としてGCするべき範囲が少なくなります。
自分の勉強の整理も兼ねて、短くまとめてみました。
正直全くGCの予備知識なしにこの記事を読んでもいまいちピンとこないと思いますので、参考にもっとわかりやすい資料を挙げています。 興味がわいた方はぜひそちらもご覧ください。
調べる中でもっと詳細を網羅的に説明している資料が紹介されていたので、自分も読めてはいませんがネクストステップとして挙げておきます。