Boehm GC ライブラリを使って C/C++ でもガベージコレクションしよう

作成日:2002.12.18


ファイナライザ(Finalizer)

GC_malloc によって割り付けられたオブジェクトは、 回収のタイミング呼び出される特別な関数を登録することが可能です。
このような関数は、ファイナライザ(Finalizer)と呼ばれます。
GC_mallocなどで確保したオブジェクトに対して、 次の API を実行することでオブジェクトにファイナライザを登録することが できます。
void GC_register_finalizer(GC_PTR obj, 
                           GC_finalization_proc fn, 
                           GC_PTR cd, 
                           GC_finalization_proc *ofn, 
                           GC_PTR *ocd);
基本的には GC_register_finalizer を使って登録を行うことで、 obj がどこからも参照されないごみオブジェクトとなった時に、 fn が呼び出されることになります。
fn は、 以下の関数宣言を持ちます。
typedef void (*GC_finalization_proc)(GC_PTR obj, GC_PTR client_data);
GC_register_finalizer を呼び出した時の cd も GC 情報中に保存され、 ファイナライザが呼び出されたときに client_data の値となります。

登録できるファイナライザは 1 オブジェクトに対して 1 つ(fncdを1組だけ)だけです。
そのため、 すでにファイナライザが登録されているオブジェクトに 再度ファイナライザを登録しようとした場合には 上書きされます。 この時、 ofnocd に退避先を指定して置くと、 古いファイナライザ情報が書き込まれます。 古いファイナライザ情報が不要な場合は、 ofnocd は NULL を指定することができます。


また、 GC_register_finalizerfn を NULLで呼び出すことによって、 1度登録されたファイナライザを解除することもできます。


ファイナライザの書き方

ファイナライザは、 GC_finalization_proc の宣言に合わせて自由に
void my_finalizer(GC_PTR obj, GC_PTR client_data) {

  // 終了処理の内容をここに書く。

}
obj にガーベージとなったオブジェクトのアドレスが渡され、 client_data には、 GC_register_finalizer による登録時の cd が入ります。

基本的に何を書いてもよいのですが、 いくつか注意点があります。
  1. ファイナライザ中で GC_malloc を呼び、メモリを確保することが可能です。
    この結果 メモリ不足が起きるとファイナライザ中で 入れ子の GC が発生します。
    ただし、 入れ子の GC の中ではファイナライザ呼び出しは起きません。 入れ子 GC 中でガーベージとなったガーベージ分のファイナライザ呼び出しは 親のファイナライザが終了するまで遅延されます。 つまり、 1度に呼び出されるファイナライザはシステム中で1つだけとなります。

  2. Java 言語では、 finalizeメソッド中で this ポインタをどこかの参照に書き込むことによって、 GC で回収されそうなオブジェクトを復活させる手法が有効でした。 しかし、 Boehm GC においてはこのような手法が使えません。 Boehm GC はファイナライザが呼ばれたオブジェクトは そのまま回収されてしまいます。 obj をどこかに書き残すと、 そのポインタは dangling pointer となり危険です。
    obj に再度、 GC_register_finalizer を登録するのもダメです。

  3. マルチスレッドプログラム中で Boehm GC ライブラリを使う場合、 ファイナライザを実行するスレッドは GC を引き起こしたスレッドになります。 そのため、どのスレッドから呼び出されても問題が発生しないように コーディングする必要があります。

ファイナライザの呼び出し順序の問題

Boehm GC ライブラリのファイナライズ呼び出しは GC 処理の最期に、 GC を引き起こしたスレッドによって実行されます。
その GC 中に回収されるオブジェクトに登録された ファイナライザが呼び出されていくのですが、 ファイナライザには呼び出し順序(というか制約)があります。

Boehm GC のファイナライザの呼び出しは トポロジカル順序(Toplogically ordering) というルールに従います。 これは、 親子関係のあるオブジェクトが同時にガーベージになった場合、 親オブジェクトからファイナライザを起動して行くというものです。
このような順序を採用する理由は、 子オブジェクトのファイナライザ実行すると、 親オブジェクトのファイナライザの番で、 ファイナライズが完了した後の子オブジェクトへのアクセスが可能な 論理的な危険性があるためです。
詳しいルールは ここを参照してください。

この順序は、 以下のような方法によってオブジェクトを識別することによって実現しています。
  1. まず スレッドのスタックフレームなどから直接参照されている オブジェクトを「生きている」とみなします。 また、「生きている」オブジェクトから間接参照されている オブジェクトも「生きている」とみなせます。
    「生きている」とみなせなかったオブジェクトは、 すべて「死んでいる」のでごみオブジェクト(ガーベージ) となります。

  2. 次に、ガーベージの中でファイナライザを登録していたものを探します。
    ここで、ファイナライザを登録したガーベージは「死んでいる」のですが、 ここから参照されているオブジェクトは 1. で「死んでいると」判断されても、 強制的に「生きている」に変更されます。 こうして「生きている」と更新された オブジェクトから間接参照されるオブジェクトも 「生きている」と書き改められます。

  3. 2. の処理を行うことで、 ファイナライズ処理を行うはずだったガーベージが 生き返ることがあります。 このようにして生き返ったファイナライザ登録オブジェクトは、 他のファイナライザ登録オブジェクトから参照されていることになります。
    この場合、生き返ったファイナライザ登録オブジェクトは、 「子」にあたるオブジェクトと判断されます。 子オブジェクトは、今回は回収を見送りファイナライザも起動しません。

  4. ファイナライザ登録されたオブジェクトのうち 生き返らなかったガーベージは「親」と判断されます。 今回の GC では、 このガーベージ分のファイナライザを起動し、 完了後 回収します。
上のようなアルゴリズムを用いているため、 いくつか問題が発生します。


第1に、 ファイナライズ処理待ちのガーベージからリンクされているオブジェクトは、 今回の GC では回収できず 次回の GC まで回収が遅延することになります。 この点に気をつけないと、 プログラマの意図した通りにオブジェクトの回収が進まない 危険性があります。
特に、 ファイナライザ登録オブジェクトが多段のリンク構造を作っていると、 回収に非常に時間がかかり、 n 段のリンクを作っていると全体を回収するのに最低 n 回の GC が 必要になります。


第2に、 ファイナライザ登録オブジェクトが互いに参照しあう循環参照が生じていると、 永遠に解法されないことがあります。
Boehm GC では この問題を回避するため 循環参照するファイナライザ登録オブジェクトが そのままガーベージになることを認めていません。 GC 時に循環参照するファイナライザ登録オブジェクトを見つけると、 警告メッセージを出し続けます。 自分自身をリンクする自己循環リンクも許されません。
循環参照が生じたファイナライザは起動されずに、 そのままオブジェクトが回収されるようです。

これを回避するために、 以下のような GC_register_finalizer() の派生バージョンがあります。
関数 GC_register_finalizer_ignore_self()
この関数は、とりあえず自己循環のみは認めるようにした GC_register_finalizer() です。

関数 GC_register_finalizer_no_order()
この関数は、 トポロジカル順序を採用しないファイナライザを登録する GC_register_finalizer() です。
上の処理でいう 2. と 3. の処理がなくなります。 そのため、 回収対象となったすべてのオブジェクトの ファイナライザが 1 回の GC で起動されます。

Java の finalize() のような処理を期待するなら、 GC_register_finalizer_no_order() を使ったほうがよいでしょう。

ファイナライザの呼び出し順序に関係する項目として、 int GC_java_finalization変数があります。 この変数の値を変更することで、 トポロジカル順序と非トポロジカル順序を更新できるとありますが、 Boehm GC のより高度な機能を呼び出さない場合、 2 つの処理の結果は同じになります。


ファイナライズに関するその他の機能

その他、ファイナライズ関係で以下のような機能があります。
変数 int GC_finalize_on_demand
この変数に 0 以外の値を挿入すると、 ユーザーが明示的に GC_invoke_finalizers() を 呼び出すまでファイナライザの呼び出しが遅延されます。 ファイナライザが呼び出されまでガーベージも回収されません。 そのため、 ユーザーが GC_invoke_finalizers() を忘れていると、 メモリ不足が発生する危険性があります。

デフォルトではオフ(0)となっています。

関数 GC_invoke_finalizers()
呼び出し待ちファイナライザを順番に処理します。 すべてのファイナライザ処理が完了すると、 GC_invoke_finalizers() も終了します。
GC_finalize_on_demand が 0 以外の場合のみ有効です。

変数 void (* GC_finalizer_notifier)()
この関数ポインタ変数にセットした関数は、 各 GC に 1回づつファイナライザ呼び出し処理が行われる前に呼び出されます。 ただし、 ファイナライザが登録されたオブジェクトが 1つも回収されない場合には呼び出されません。
この機能を使うことによって、 ファイナライザが呼び出される前に、 ファイナライザを実行スレッドの情報を収拾し準備を行うことができます。
ただし、 この機能は GC_finalize_on_demand が 0 以外の時のみ 有効なようです。

関数 void GC_finalize_all()
強制的にすべてのファイナライザを呼んでしまう機能です。
この関数は javaxfc.h をインクルードする必要があります。


戻る
Written by NAKAMURA Minoru, Email: nminoru atmark nminoru dot jp, Twitter:@nminoru_jp