Valaのメモリー管理について
久しぶりにValaのドキュメントの翻訳。前から気になっていたGCに関するものです。
原典は、“Vala's Memory Management / Vala/ReferenceHandling (last edited 2009-11-18 18:39:23 by Frederik)Explained”です。
******************************
Valaのメモリー管理について
Valaは、ガベージコレクション〔訳注:とくにマーク・アンド・スイープ方式など他のGCアルゴリズムをさしていると思われます。〕のかわりに自動参照カウント方式を、メモリー管理の基礎としています。
この方式には有利な点と不利な点とがあります。参照カウント方式は決定論的deterministicですが〔訳注:これもマーク・アンド・スイープ方式との対比です。参照カウント方式ではオブジェクトのメモリー空間からの削除が速やかに実施できます。〕、いくつかのケースでは循環参照問題を発生させることがあります。これらのケースでは、あなたはこの循環を断ち切るために弱参照を使用しなくてはなりません。このためにValaで用意されているキーワードはweakです。
参照カウントというものがどのようなものか見てみましょう:
参照型オブジェクトが変数に割り当てられる(参照される)たび、そのオブジェクトの内部参照カウントには1が加算されます(ref)。そして参照変数がスコープから外れると、オブジェクトの内部参照カウントからは1が減算されます(unref)。
もしあるオブジェクトの参照カウントが0になると、そのオブジェクトは“解放され”ます〔訳注:そのオブジェクトのために用意されたメモリー領域は解放されます〕。あるオブジェクトが“解放された”とき、そのオブジェクトのメンバーである参照型データの参照カウントからも1が減算されます(unref)。それらが0になったとき、その参照型データもまた同じ処理を経て“解放”されます。以下くりかえし。
では循環参照とは何でしょうか? そしてそれはなぜ問題なのでしょうか? データを持たないシンプルな二重結合リストの実装について見てみましょう:
class Node : Object { public Node prev; public Node next; public Node (Node? prev = null) { this.prev = prev; // ref。参照カウント+1 if (prev != null) { prev.next = this; // ref。参照カウント+1 } } } void main () { var n1 = new Node (); // ref。参照カウント+1 var n2 = new Node (n1); // ref。参照カウント+1 // 2つのオブジェクトの参照カウントを出力します。 stdout.printf ("%u, %u\n", n1.ref_count, n2.ref_count); } // unref、unref。参照カウント-1、参照カウント-1
この例では、よりよい理解のため、参照と参照解除が発生している箇所にコメントをしています。次の図は、n1とn2が変数に割り当てられたあとの状況を示しています:
この図のなかで矢印のそれぞれは参照を、そしてノードのそれぞれは数字を持ち、そのオブジェクトの参照カウント(矢印に指されている数)をあらわしています。ブロックの終わりでn1とn2の参照が解除されたとき、2つのノードの参照カウントは1になるはずです。しかしノードの数字が0になることはなく、オブジェクトは解放されません。
〔先ほどのプログラム例では〕さいわいにも、いずれにせよプログラムが終了することで、OSがすべてのメモリーを解放してくれるはずです。しかしプログラムが長時間動き続けた場合、何が起きるでしょうか? 例えば:
void main () { while (true) { var n1 = new Node (); var n2 = new Node (n1); Thread.usleep (1000); } }
タスクマネージャーもしくはプロセスモニター(例えばgnome-system-monitorです)を開いてから、上記プログラムを起動します。あなたはプログラムがメモリーを使い果たしていく様を目撃することになるでしょう。システムがスローダウンする前に、プログラムを終了させてください。(即座にメモリーが解放されます。)
このような問題はC#やJavaでは起きません。ガベージコレクターはランタイム上に発生した循環を検知することができるのです。しかしValaでは、循環参照問題に際して対抗手段をとる必要があります。
私たちは、循環する参照の一方を弱参照にすることで、循環を断ち切ることができます:
public weak Node prev; public Node next;
こうすることにより、当該の変数への割り当てが、オブジェクトの参照カウントの加算を引き起こさなくなります。この状態を図にすると次のようになります:
2重結合リストの最初のオブジェクト〔訳者:図中で左側のノード〕の参照カウントは、最前のときは2でしたが、今度は1です。ブロックの終わりでn1とn2がスコープを外れたとき、2つのオブジェクトの参照カウントからは1が減算されます。最初のオブジェクトは解放されます。なぜなら参照カウントは0になるからです。このとき、2つめのオブジェクトの参照カウントは1になっています。しかし〔今まさに解放されつつある〕最初のオブジェクトは2つめのオブジェクトへの参照を保持していますから、〔最初のオブジェクトの解放にともない〕2つ目のオブジェクトの参照カウントは再度減算されます。そうしてカウントが0になって、2つめのオブジェクトもまた解放処理の対象になるのです。
もう一度〔訳者:weakキーワードを使用しているバージョンの〕プログラムを動かしてみましょう。今度はメモリー消費が一定に保たれているのを確認できるはずです。
循環参照は、必ずしも直接の循環に限られません。次のような例も同じく循環参照です:
持ち主のいない参照
すべてのValaのクラス・オブジェクトと、ほとんどのGObjectベースのライブラリのオブジェクトは、参照をカウントしています。しかしながら、ValaではGObjectベースでないCライブラリも使用できます。それらはデフォルトでは参照カウントをサポートしていません。([Compact]属性でアノテーションされている)これらのクラスはコンパクトクラスと呼ばれています。
参照カウントが行われないオブジェクトは、単一の強い参照を持ちます(この参照は“マスター”参照のように考えられます)。この参照がスコープから外れたとき、そのオブジェクトは解放されます。その他すべての参照は所有者不在の参照です。これらの参照がスコープを外れても、問題のオブジェクトは解放されません。
あなたが必要とするあるオブジェクトへの所有者不在の参照(これはメソッドの返値型のunownedキーワードにより識別できます)を返すメソッド(たいがい非GObjectライブラリの)を呼ぶとき、2つの選択肢があります: それがcopyメソッドをもっているならばそのオブジェクトのコピーを取得する。これによりあなたは、複製した新しいオブジェクトを指す、あなたを所有者とする強参照を得ることができます。あるいは、その所有者不在の参照を、unownedキーワードとともに宣言された変数に割り当てる。これによりValaはこのオブジェクトが解放されてはならないものであるということを認識します。
Valaはあなたが所有者不在の参照を、強参照(すなわち所有者不在でない参照)に割り当てることを阻止します。しかしながら、あなたは(owned)により所有者名義を別の参照に書きかえることことができます:
[Compact] class Foo { } void main () { Foo a = new Foo (); unowned Foo b = a; Foo c = (owned) a; // 'c' is now the new "master" reference“c”は新しい“マスター”参照になる }
ところで、Vala文字列もまた参照カウントを持ちません。これらがC言語のchar*型を基礎としているからです。しかしValaは必要に応じて自動的にコピーを行います。あなたはこの問題について何か心配することはないのです。バインディングの作成者だけが、文字列の参照が所有者不在のものかそうでないのかについて気をつけ、必要に応じてAPIのなかにマーキングをしておけばよいのです。
現在のところValaバインディングAPIの多くの型は、本来unownedとマークされなくてはならないのに、誤ってweakとマークされています。以前は2つの参照形態を指すのにweakというキーワードしかなかったためです。これらを混同しないでください。当面はweakキーワードとunownedキーワードは、相互置換可能な形で使用できます。しかしあなたは、weakは循環参照を断ち切るためにだけ、またonownedは上述の所有関係問題のためにだけ使うようにしてください。
メモリー管理の例
通常のケース、参照カウントをサポートするクラス
public class Foo { public void method () { } } void main () { Foo foo = new Foo (); //オブジェクトがメモリーに割り当てられ、参照カウントが加算される foo.method (); Foo bar = foo; // 参照カウントが加算される } // 減算、減算 → オブジェクトは解放される
すべては自動的に管理されます。
ポインター構文による手動メモリー管理
もしメモリー管理のフルコントロールを必要としているならば、いつでも手動メモリー管理方式を選択することができます。ポインター構文はC++から借用されたものです:
void main () { Foo* foo = new Foo (); // メモリーに割り当てられる foo->method (); Foo* bar = foo; delete foo; // メモリーが解放される }
参照カウントをともなわないコンパクトクラスのためのメモリー管理
コンパクトクラスはValaの型システムには登録されていないクラスです。それらは大抵、GObjectベースでない、Cライブラリにより提供されるクラスです。とはいえ、もしあなたがコンパクトクラスが機能面で非常に制限されたものであること(例えば、継承やインターフェースの実装、privateフィールドがないことなど)を受けれるのであれば、Vala言語を用いて独自のそれを定義することも可能です。これらのクラスはとても軽量です。
コンパクトクラスは、デフォルトでは参照カウントをサポートしていません。したがって〔そのインスタンスは〕ただ一つの“マスター”参照のみを持ちます。これにより問題のオブジェクトは、スコープの終了とともに解放されてしまいます。他のすべての参照は所有者不在の参照になってしまうのです。
[Compact] public class Foo { public void method (); } void main () { Foo foo = new Foo (); // メモリー領域が割り当てられる foo.method (); unowned Foo bar = foo; // 訳者:変数barはFooインスタンスへの強参照を得られません。 Foo baz = (owned) foo; /* 所有権が移されます: ここでbazは“マスター”になります。*/ unowned Foo bam = baz; // 訳者:同様に変数bamも強参照を得られません。 } // 解放されます(“マスター”参照であるbazがスコープを外れたからです)
参照カウントをともなうコンパクトクラスのためのメモリー管理
コンパクトクラスに自分で参照カウントを実装してしまうこともできます。そのためにはCCode属性により参照と参照解除のために使用されるべき関数をそれぞれValaに教えてやる必要があります。
[Compact] [CCode (ref_function = "foo_up", unref_function = "foo_down")] public class Foo { public int ref_count = 1; public unowned Foo up () { ++ref_count; return this; } public void down () { if (--ref_count == 0) { delete (void*) this; } } public void method () { } } void main () { Foo foo = new Foo (); // メモリー領域が割り当て、参照カウント+1 foo.method (); Foo bar = foo; // 参照カウント+1 } // 参照カウント-1、参照カウント-1 → 解放される
このように、再びすべては自動的に管理されるようになります。
参照カウントはないもののcopyメソッドをともなうイミュータブルなコンパクトクラスのためのメモリー管理
もしあるコンパクトクラスが参照カウントをサポートしておらず、イミュータブルで(その内部状態は変化しません)、しかもそのオブジェクトを複製するcopyメソッドを持っているとき、Valaはそのオブジェクトが強参照に割り当てられる際に、自動的にcopyを行います。
[Compact] [Immutable] [CCode (copy_function = "foo_copy")] public class Foo { public void method () { } public Foo copy () { return new Foo (); } } void main () { Foo foo = new Foo (); // メモリー領域が割り当て foo.method (); Foo bar = foo; // 複製 } // 2つとも解放
もし微調整のためにコピーを阻止したいのであれば、所有者不在の参照も使用できます。
void main () { Foo foo = new Foo (); // メモリー領域が割り当て foo.method (); unowned Foo bar = foo; // 複製はされない } // 解放