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; // 複製はされない } // 解放
JavaプログラマーからみたVala(7) その他
原典は、“Vala for Java Programmers”(2011年3月13日取得)です。
- JavaプログラマーからみたVala (1) ソースコード
- JavaプログラマーからみたVala(2) 型
- JavaプログラマーからみたVala (3) クラス
- JavaプログラマーからみたVala (4) クラス②
- JavaプログラマーからみたVala(5) 通知
- JavaプログラマーからみたVala(6) 引数
- JavaプログラマーからみたVala(7) その他
- WindowsでValaを動かす
******************************
構造体
構造体(struct)は、Javaには存在しない概念です。構造体とクラスのちがいを理解してもらうために、Point型の2つの実装をご覧に入れます。実装の一つはクラスとして、もう一つは構造体としてなされています。
class Point { public x; public y; public Point (int x, int y) { this.x = x; this.y = y; } public void print () { stdout.printf ("(%d, %d)", this.x, this.y); } } struct Point { public x; public y; public Point (int x, int y) { this.x = x; this.y = y; } public void print () { stdout.printf ("(%d, %d)", this.x, this.y); } }
ご覧のとおり、上記の2つの実装において、ちがうのはただstruct/classというキーワードのみです。にもかかわらず構造体はプログラムの動作においてクラスと異なっています。
// Class var p1 = new Point (2, 4); // p1がヒープに配置される var p2 = p1; // 参照の代入 p2.x = 3; p1.print (); // => (3, 4) p1の状態が変化している p2.print (); // => (3, 4) var p3 = new Point (3, 4); p2 == p3; // => 結果はfalse。(格納された値は同じでも参照が異なる) // Struct var p1 = Point (2, 4); // p1はスタックに配置される var p2 = p1; // p1のコピーがp2に代入される p2.x = 3; p1.print (); // => (2, 4) p1は変化しない p2.print (); // => (3, 4) var p3 = Point (3, 4); p2 == p3; // => 結果はtrue。 (同じ値だから)
構造体はヒープに配置されません。(まさしくそれが、構造体の生成にnew演算子が使用されない理由です。)何よりも重要なちがいは、変数に代入される際にコピーされる点です。構造体は値型であり、参照型ではないのです。基本的にintやdoubleなどと同様に振る舞うということです。
構造体は継承やシグナル、インターフェースの実装をサポートしていません。
同期化
Javaの場合: synchronized修飾子があります。
Valaの場合: lockの仕組みは、メソッドに対して適用することができません。適用できるのはメンバーオブジェクトに対してだけです。*1
条件的コンパイル
Javaの場合: 条件的コンパイルの仕組みは存在しません。*2
Valaの場合: #if、#elif、#else、#endifを使用したシンプルな条件指定が可能です。しかしながらC言語のプリプロセッサーのような、マクロプリプロセッサー(macro preprocessor)とは異なります。
条件的コンパイルのサンプル(Conditional Compilation Example)を参照してください。
メモリー管理
Javaの場合: ガーベージコレクションの仕組みがあります。
Valaの場合:自動参照カウントの仕組みがあります。
これにはメリットとデメリット(advantages and disadvantages)があります。参照カウント方式は決定論的ですが、いくつかのケースで循環参照におちいる危険があります。このような場合に循環を断ち切る方策として、開発者は弱参照(weak references)を使用できます。Valaでは弱参照のためにweakキーワードが用意されています。
「説明されているValaのメモリー管理」(Vala's Memory Management Explained)を参照してください。
staticな初期化処理
Javaの場合: staticな初期化ブロックがあります。このブロックはインスタンスが生成されるか、staticなメンバーが参照されたときに呼び出されます。
public class Foo { static { System.out.println("Static initializer block invoked."); } }
Valaの場合: static construct { } ブロックがあります。クラスもしくはそのクラスの直接/間接のサブクラスがインスタンス化されるとき、このブロックも呼び出されます。このブロックは、プログラム内でクラスが使用される際に、かならず1回だけ呼び出されることが保証されています。
class Foo : Object { static construct { stdout.printf ("Static constructor invoked.\n"); } }
加えてVala言語のクラスはconstruct { } ブロックを持つことができます。このブロックはクラスが最初に使用される際に1回実行されます。またこのクラスのサブクラスが最初に使用される際にも1回実行されます。
可変長引数(Varargs)
Javaの場合:
String format(String pattern, Object... arguments) { // ... }
Valaの場合: C言語スタイルの可変長配列が使用できます。タイプセーフではありません。
string format (string pattern, ...) { var l = va_list (); // ... }
より多くの情報が、「Valaチュートリアル」で見られます。タイプセーフな可変長配列は、paramsキーワードとともに計画されています。
ネイティブコール
Javaの場合: nativeキーワードとJNIの仕組みを使用します。
Valaの場合: すべてのメソッドは“native”コールです。C言語の関数をバインディングするには、手っ取り早くするならexternキーワードを、そうでなければ.vapiファイルを使用します。呼び出したいC言語で書かれた関数の名前を指定するにはCCode属性を使用します(Vala言語で記述するメソッド名とちがう場合だけです)。
public class MainClass : Object { [CCode (cname = "SampleMethod")] public static extern int sample_method (int x); static void main () { stdout.printf ("sample_method () returns %d\n", sample_method (5)); } }
コンパイラには、-X –l...オプションとともにライブラリの名前を渡します。(-Xオプションはこのオプションの次のオプションがCコンパイラに渡されるものであることをあらわします。)
$ valac demo.vala -X -lfoo
外部メソッド(関数)が定義されたC言語のソースファイルをValaコンパイラに対して渡すこともできます。これによりVala言語とC言語のソースコード・プロジェクトを混合させることができます。
$ valac demo.vala foo.c
Valaではできないこと
-
- finalクラスは存在しません。(sealedクラスが計画されています。)staticクラスは存在しません。(代わりにネストされたネームスペースを使用します。)
- 匿名内部クラスは存在しません。(代わりにデリゲートとクロージャを使用します。)
- ジェネリック型の引数に対する制約はありません。*3
- オブジェクトの文字列変換に関する暗黙の規約は存在しません。(toString()メソッドが一般的なかたちで存在しないため。)とはいえ、to_string()メソッドを持つ型は、文字列テンプレート(@”...”)によりサポートされています。*4
- 名前付きbreakとラベルの機能はありません。
- strictfpやtransient修飾子は存在しません。
このチュートリアルがカバーできていない言語の特徴
- 非同期メソッド
- 代替的なコンストラクション形態(Alternative construction scheme。GObjectスタイルのそれ)
これらについてより多くの情報を得るには、「Vala言語の一般的なチュートリアル」(general Vala tutorial)を参照してください。
******************************
※原典ではまだいくつかの節が続いていますが、いかにも書き足したという印象なのと、内容がそれほど一般的なものであるように思われないのでここに翻訳は載せません。
*1:訳者:ValaにはJavaにおけるsynchronizedブロックに対応するlockブロックが存在します。しかしこのブロックによりロックを取得できるのは、そのメソッドの所属するクラスのメンバーオブジェクト──intなども含む──についてのみです。[http://live.gnome.org/Vala/Tutorial:title=チュートリアル]のほうに例が載っています。
*2:訳者:そもそも中間コードこそが基本的な配布の単位なので。
*3:訳者:型名の<>で囲われている部分のことでしょうか?
*4:訳者:つまり文字列テンプレートでも使用可能なようなVala言語的に妥当なクラスはto_string()メソッドを実装しているべきである、ということです。
JavaプログラマーからみたVala(6) 引数
原典は、“Vala for Java Programmers”(2011年3月13日取得)です。
- JavaプログラマーからみたVala (1) ソースコード
- JavaプログラマーからみたVala(2) 型
- JavaプログラマーからみたVala (3) クラス
- JavaプログラマーからみたVala (4) クラス②
- JavaプログラマーからみたVala(5) 通知
- JavaプログラマーからみたVala(6) 引数
- JavaプログラマーからみたVala(7) その他
- WindowsでValaを動かす
******************************
引数の向き
Vala言語ではメソッドはout引数もしくはref引数を持つことができます。もしあるメソッドの引数が、メソッドのシグネチャの中でoutもしくはrefとマークされている場合、それはこのメソッドが外部から渡された変数の値を書き換えることができるということを意味します(out引数もしくはref引数しか渡せません)。そしてその変更はメソッドの処理が完了した後も有効なのです。もし渡された変数が参照型の場合、メソッドはその参照自体を書き換えられるのです(変数が参照するオブジェクトの状態を変更するというのではありません。変数が参照するオブジェクトを別のオブジェクトにすることができるのです)。
outとrefのちがいは、refの場合メソッドに渡される引数は事前に初期化済みでないといけないのに対して、outではその必要はなく、メソッド内部で初期化が実施されるということです。
これらのキーワードは、メソッドの定義コードと、メソッド呼び出しコードの双方で指定する必要があります。
/* * This method takes a normal argument, an 'out' argument * and a 'ref' argument. */ void my_method (int a, out int b, ref int c) { a = 20; // メソッド呼び出し元には効果は及ばない。 // 引数bは初期化されていない可能性がある。 b = 30; // 引数bが初期化される。呼び出し元の変数に反映される。 c = 40; // 呼び出し元の変数に反映される。 } void main () { int x = 2 int y; // 初期化されないまま。 int z = 4; // refキーワード付きでメソッドに渡す前に初期化が必要。 my_method (x, out y, ref z); stdout.printf ("%d %d %d\n", x, y, z); // => "2 30 40" // xは変更されないが、yとzは変更されている。 }
Null可能性(Nullability)
Vala言語ではメソッドの参照型引数について、それがnullでもよい場合には、クエスチョンマーク(?)を型名の後に付ける必要があります。
void my_method (Object? a, Object b) { } void main () { my_method (null, new Object()); // コンパイルできる(第1引数はnullでもよい) my_method (null, null); //コンパイルできない(第2引数はnullではいけない) }
null可能性は実行時にも(部分的には)コンパイル時にもチェックされます。このチェックはnull参照エラーを阻止する助けになります。
--enable-experimental-non-nullオプションにより、厳格なnull可能性チェック(実験的機能)を有効化することもできます。この場合、Valaはコンパイル時に、メソッド引数のみならず、すべての参照型変数をチェックします。例えば次のコードは、厳格なnull可能性チェックの下ではコンパイルできないでしょう。
void main () { string? a = "hello"; string b = a; // Error: 'a' はnullでもよいが、'b'はnullであってはいけない }
どうしてもnull可能な変数をnull不可能変数に代入したい場合(!)を使用するとコンパイルできます。
void main () { string? a = "hello"; string b = (!) a; }
引数チェック
void method(double d, int i, Foo foo) { if (d < 0 || d > 1.0) throw new IllegalArgumentException(); if (i < 0 || i > 10) throw new IllegalArgumentException(); if (foo == null) throw new IllegalArgumentException(); // ... }
Valaの場合: 参照型引数の場合、クエスチョンマークによりnull可能であることを印付けない限り、暗黙的なnullチェックが実施されます。開発者がこれをチェックするためのコードを記述する必要はありません。メソッドはその他の事前条件を持つこともできます。
void method (double d, int i, Foo foo) requires (d >= 0.0 && d <= 1.0) requires (i >= 0 && i <= 10) { // ... }
Vala言語では、事後条件(返却値についての条件)も指定することができます。例えば──
int square (int i) ensures (result >= 0) { return i * i; }
ここでresultは返却値をあらわす特別な変数です。
例外(エラー)は回復可能なエラー(データベースエラー、IOエラーなど)に対して使用します。事前条件やアサーション(assert(...))は不正な引数などプログラミングエラーに対して使用します。
******************************