メモリ管理
Dは、明示的なメモリ管理を必要としない言語だ。しかし、システムプログラマとしては、特別な場合にメモリを管理する方法を知っておくことが重要だ。
メモリ管理は非常に広範なトピックだ。この章では、ガベージコレクタ(GC)、GCからメモリを割り当てる方法、および特定のメモリ位置にオブジェクトを構築する方法についてのみ説明する。この本を書いた時点ではまだ実験段階だったstd.allocatorモジュールだけでなく、さまざまなメモリ管理手法についても調べてみることをお勧めする。
前の章と同様、以下で"変数"と表記する場合は、structおよびclassオブジェクトを含むあらゆる型の変数を指す。
メモリ
メモリは、実行中のプログラムとそのデータの両方がメモリ内に格納されるため、他のシステムリソースよりも重要なリソースだ。メモリは、最終的にはオペレーティングシステムに属しており、プログラムがそのニーズに応じて使用できるようにしている。プログラムが使用するメモリの量は、そのプログラムの当面のニーズに応じて増減する。プログラムが終了すると、そのプログラムが使用していたメモリ領域は自動的にオペレーティングシステムに戻される。
メモリは、変数の値が書き込まれた大きな紙のようなものと考えることができる。各変数は、その値が必要に応じて書き込まれたり読み出されたりする特定の場所に保持される。変数の寿命が終了すると、その場所は別の変数に使用される。
メモリを実験する場合、&(アドレス)演算子が便利だ。例えば、次のプログラムは、隣り合って定義されている2つの変数のアドレスを出力する。
注釈:プログラムを実行するたびに、アドレスは異なる値になる可能性が高い。さらに、変数のアドレスを取得するだけで、その変数をCPUレジスタに保存する最適化が無効になる。
出力からわかるように、変数の位置は4バイト離れている:
| i | 7FFF2B633E28 |
|---|---|
| j | 7FFF2B633E2C |
2つのアドレスの最後の桁は、iがjの場所のすぐ前のメモリ位置に存在することを示している。8に4(intのサイズ)を加えると12(16進表記のC)になる。
ガベージコレクタ
Dプログラムで使用される動的変数は、ガベージコレクタ(GC)が所有するメモリブロック上に存在する。変数の寿命が終了すると (つまり、その変数が使用されなくなった場合)、その変数はGCによって実行されるアルゴリズムに従って最終化される。その変数を含むメモリ位置を他に必要とするものがない場合、そのメモリは他の変数に使用するために再利用される。このアルゴリズムはガベージコレクションと呼ばれ、その実行はガベージコレクションサイクルと呼ばれる。
GCが実行するアルゴリズムは、おおまかに次のように説明できる。プログラムのルートにあるポインタ(参照を含む)によって直接的または間接的に到達可能なすべてのメモリブロックがスキャンされる。到達可能なメモリブロックは"まだ使用中"とタグ付けされ、それ以外のメモリブロックは"使用済み"とタグ付けされる。アクセスできないブロックにあるオブジェクトおよび構造体のファイナライザが実行され、それらのメモリブロックは将来の変数に使用するために再利用される。ルートは、すべてのスレッドのプログラムスタック、すべてのグローバル変数およびスレッドローカル変数、およびGC.addRootまたはGC.addRangeによって追加された追加データとして定義される。
一部のGCアルゴリズムは、オブジェクトをメモリ内の同じ場所にまとめるために移動させることがある。プログラムの正しさを保つため、そのようなオブジェクトを指すすべてのポインタ(および参照)は、自動的に新しい位置を指すように変更される。Dの現在のGCはこれを行わない。
GCが"正確"であるとは、どのメモリがポインタを含むかを含まないかを正確に知っていることを意味する。GCが保守的であるとは、すべてのメモリをポインタとしてスキャンすることを意味する。DのGCは部分的に保守的で、ポインタを含むブロックのみをスキャンするが、それらのブロック内のすべてのデータをスキャンする。このため、一部のブロックは収集されず、そのメモリが"リーク"する可能性がある。大きなブロックは"偽のポインタ"の対象になりやすい。この問題を回避するため、使用しなくなった大きなブロックを手動で解放することをおすすめする場合がある。
ファイナライザの実行順序は不特定だ。例えば、オブジェクトの参照メンバーは、そのメンバーを含むオブジェクトよりも先にファイナライズされる場合がある。そのため、動的変数を参照するクラスメンバーは、デストラクタ内でアクセスしてはならない。これは、C++などの言語の決定的な破棄順序とは大きく異なることに注意しよう。
ガベージコレクションサイクルは、より多くのデータ用のスペースを見つける必要があるなど、さまざまな理由で開始される。GCの実装によっては、ガベージコレクションサイクル中に新しいオブジェクトを割り当てると、コレクションプロセス自体が妨害される可能性があるため、コレクションサイクル中は実行中のすべてのスレッドを停止しなければならない場合がある。これは、プログラムの実行の遅延として観察されることがある。
ほとんどの場合、プログラマはガベージコレクションプロセスに干渉する必要はない。ただし、core.memoryモジュールで定義された関数を使用して、必要に応じてガベージコレクションサイクルを遅延またはディスパッチすることは可能だ。
ガベージコレクションサイクルの開始と遅延
プログラムの応答性が重要な部分では、ガベージコレクションサイクルの実行を遅らせたい場合がある。GC.disableはガベージコレクションサイクルを無効にし、GC.enableは再び有効にする。
ただし、GC.disableによってガベージコレクションサイクルの実行が確実に防止されるわけではない。GCがOSからより多くのメモリを取得する必要があるが、取得できない場合、GCは、利用可能なメモリを確保するための最後の手段として、ガベージコレクションサイクルを実行する。
不特定の時間に自動的に実行されるガベージコレクションに頼る代わりに、GC.collect()を使用してガベージコレクションサイクルを明示的に開始することができる。
通常、GCはメモリブロックをオペレーティングシステムに返さない。プログラムが将来必要とする場合に備えて、それらのメモリページを保持する。必要に応じて、GC.minimize()を使用して、GCに未使用のメモリをオペレーティングシステムに返すよう依頼することができる。
メモリの割り当て
システム言語では、オブジェクトが格納されるメモリ領域を指定することができる。このようなメモリ領域は、一般にバッファと呼ばれる。
メモリを割り当てる方法はいくつかある。最も簡単な方法は、固定長配列を使用することだ:
bufferは、100バイトのメモリ領域として使用できる状態になっている。ubyteの代わりに、型と関連付けずに、voidの配列としてこのようなバッファを定義することもできる。voidには値を割り当てることができないため、.init値も持つことができない。このような配列は、特別な構文=void
この章では、メモリを予約するためにcore.memoryモジュールからGC.callocだけを使う。このモジュールには、さまざまな場面で役立つ他の機能も多数ある。さらに、C標準ライブラリのメモリ割り当て関数は、core.stdc.stdlibモジュールでも使える。
GC.calloc 指定したサイズのメモリ領域を、すべて0の値で事前に入力して割り当て、割り当てた領域の開始アドレスを返す。
通常、返されるvoid*の値は、適切な型のポインタにキャストされる。
しかし、通常はその中間ステップは省略され、戻り値は直接キャストされる。
100のような任意の値ではなく、通常、メモリ領域のサイズは、必要な要素の数と各要素のサイズを乗算して計算される。
クラスには重要な違いがある。クラス変数のサイズとクラスオブジェクトのサイズは同じではない。.sizeofはクラス変数のサイズで、常に同じ値になる。64ビットシステムでは8、32ビットシステムでは4だ。クラスオブジェクトのサイズは、__traits(classInstanceSize)で取得する必要がある。
要求されたサイズをシステムに十分なメモリがない場合、core.exception.OutOfMemoryError例外がスローされる。
そのくらいの空き容量がないシステムでの出力:
core.exception.OutOfMemoryError
GCから割り当てられたメモリ領域は、GC.freeを使用してGCに返すことができる:
ただし、free()を呼び出しても、そのメモリブロック上に存在する変数のデストラクタが必ず実行されるとは限らない。デストラクタは、各変数に対してdestroy()を呼び出すことで明示的に実行することができる。GCコレクションまたは解放中に、classおよびstruct変数のファイナライザを呼び出すには、さまざまな内部メカニズムが使用されることに注意しよう。これらを確実に呼び出す最善の方法は、変数を割り当てる際にnew演算子を使用することだ。その場合、GC.freeがデストラクタを呼び出す。
プログラムは、以前に割り当てられたメモリ領域がすべて使用済みで、追加のデータ格納スペースがないと判断する場合がある。GC.reallocを使用すると、以前に割り当てられたメモリ領域を拡張できる。realloc()は、以前に割り当てられたメモリポインタと新たに要求されたサイズを受け取り、新しい領域を返す:
realloc() 必要最小限のメモリを割り当てるように効率化されている:
- 古い領域の後のメモリ領域が他の目的で使用されておらず、新しい要求を満たす十分なサイズの場合、
realloc()はそのメモリ領域を古い領域に追加し、バッファをインプレイスで拡張する。 - 古い領域の後のメモリ領域が既に使用中であるか、十分なサイズでない場合、
realloc()は新しいより大きなメモリ領域を割り当て、古い領域の内容を新しい領域にコピーする。 nullをoldBufferとして渡すことも可能で、その場合realloc()は単に新しいメモリを割り当てる。- 以前のサイズよりも小さいサイズを渡すことも可能で、その場合、古いメモリの残りの部分はGCに返される。
- 新しいサイズとして0を渡すことも可能で、その場合は
realloc()は単にメモリを解放する。
GC.reallocは、C標準ライブラリの関数realloc()を応用したものだ。このような複雑な動作をするため、realloc()は関数インターフェースの設計が悪いとされている。GC.reallocの意外な点は、元のメモリがGC.callocで割り当てられていた場合でも、拡張部分は決してクリアされないことだ。そのため、メモリをゼロ初期化することが重要な場合は、以下のような関数reallocCleared()が便利だ。blockAttributesの意味については、後で説明する。
上記の関数は、core.stdc.stringモジュールにあるmemset()を使用して、新たに拡張されたバイトをクリアしている。memset()は、ポインタと長さで指定したメモリ領域のバイトに、指定した値を代入する。この例では、extendedPartのextendedLengthバイトに0を代入している。
以下の例でreallocCleared()を使う。
同様の関数であるGC.extendの動作は、realloc()ほど複雑ではなく、上記の最初の項目のみを適用する。つまり、メモリ領域をその場で拡張できない場合、extend()は何も行わず、0を返す。
メモリブロックの属性
GCアルゴリズムの概念と手順は、enum BlkAttrによってメモリブロックごとにある程度設定することができる。BlkAttrは、GC.callocおよびその他の割り当て関数のオプションパラメータだ。このパラメータは、以下の値で構成される。
NONE: 値 0。属性を指定しない。FINALIZE: メモリブロックに存在するオブジェクトを最終化することを指定する。通常、GCは、明示的に割り当てられたメモリ位置に存在するオブジェクトの寿命はプログラマによって制御されていると想定し、そのようなメモリ領域にあるオブジェクトは破棄しない。
GC.BlkAttr.FINALIZEは、GCにオブジェクトのデストラクタを実行するよう要求するためのものだ。FINALIZEは、ブロックに適切に設定された実装の詳細に依存することに注意しよう。これらの詳細の設定は、new演算子を使用してGCに任せることを強くお勧めする。NO_SCAN: メモリ領域がGCによってスキャンされないように指定する。メモリ領域内のバイト値は、メモリの他の部分にある無関係なオブジェクトへのポインタのように見えることがある。その場合、GCは、それらのオブジェクトの実際の寿命が終了した後も、それらがまだ使用中であるとみなしてしまう。
オブジェクトポインタを含まないことがわかっているメモリブロックは、
GC.BlkAttr.NO_SCAN:そのメモリブロックに配置された
int変数は、オブジェクトポインタと間違われる心配なく、任意の値を指定することができる。NO_MOVE: メモリブロック内のオブジェクトを他の場所に移動しないことを指定する。APPENDABLE: Dランタイムが高速な追加処理を支援するために使用する内部フラグ。メモリを割り当てる際にはこのフラグを使用しないようにしよう。NO_INTERIOR: ブロックの最初のアドレスへのポインタのみが存在することを指定する。これにより、ブロックの中間部分へのポインタはポインタの追跡時にカウントされないため、"偽のポインタ"を削減できる。
enum BlkAttrの値は、ビット演算の章で見たビットフラグとして使用するのに適している。|演算子によって2つの属性をマージする方法は、次の通りだ。
当然のことながら、GCは、自身の関数によって予約されているメモリブロックのみ認識し、それらのメモリブロックのみをスキャンする。例えば、core.stdc.stdlib.callocによって割り当てられたメモリブロックについては認識しない。
GC.addRangeは、GCに無関係のメモリブロックを導入するためのものだ。補完関数GC.removeRangeは、core.stdc.stdlib.freeなど、他の手段でメモリブロックを解放する前に呼び出す必要がある。
場合によっては、メモリブロックがGCによって予約されていても、プログラム内にそのメモリブロックへの参照がないことがある。例えば、メモリブロックへの唯一の参照がCライブラリ内に存在する場合、GCは通常その参照を認識せず、そのメモリブロックは使用されなくなったものとみなす。
GC.addRootは、収集サイクル中にスキャンされるように、メモリブロックをルートとして GCに導入する。そのメモリブロックから直接または間接的に到達できるすべての変数は、存続中としてマークされる。メモリブロックが使用されなくなった場合は、補完関数GC.removeRootを呼び出す必要がある。
メモリ領域の拡張例
配列のように機能する単純なstructテンプレートを設計しよう。例を短くするため、要素の追加とアクセス機能のみを提供する。配列と同様に、必要に応じて容量を増やそう。次のプログラムは、上記で定義したreallocCleared()を使用している。
配列の容量は約50%増加する。例えば、100個の要素の容量が消費された後、新しい容量は151になる。(余分な1は、長さが0の場合、50%を追加しても配列が拡大しない場合のためだ。)
次のプログラムは、double型でそのテンプレートを使用している。
出力:
インデックス0の要素を追加
容量を0から1に増やす
インデックス1の要素を追加
容量を1から2に増やす
インデックス2の要素を追加
容量を2から4に増やす
インデックス3の要素を追加
インデックス4の要素を追加
容量を4から7に増やす
インデックス5の要素を追加
インデックス6の要素を追加
インデックス7の要素を追加
容量を7から11に増やす
インデックス8の要素を追加
インデックス9の要素を追加
| 0 | 1.1 | 2.2 | 3.3 | 4.4 | 5.5 | 6.6 | 7.7 | 8.8 | 9.9 |
アラインメント
デフォルトでは、すべてのオブジェクトは、そのオブジェクトの型に固有の量の倍数であるメモリ位置に配置される。その量は、その型の整列と呼ばれる。例えば、整数型intの整列は4だ。これは、整数型int変数は、4の倍数(4、8、12など)のメモリ位置に配置されるためだ。
アライメントは、CPUのパフォーマンスや要件のために必要だ。アライメントがずれたメモリアドレスにアクセスすると、アクセス速度が低下したり、バスエラーが発生したりする可能性があるからだ。さらに、特定の型の変数は、アライメントが正しいアドレスでのみ正しく動作する。
.alignofプロパティ
型の.alignofプロパティは、その型のデフォルトの整列値だ。クラスでは、.alignofはクラスオブジェクトではなく、クラス変数の整列だ。クラスオブジェクトの整列は、std.traits.classInstanceAlignmentで取得できる。
次のプログラムは、さまざまな型の配置を表示する。
プログラムの出力は環境によって異なる場合がある。以下の出力を参照しよう:
| サイズ | アラインメント | 型 |
|---|---|---|
| 1 | 1 | char |
| 2 | 2 | short |
| 4 | 4 | int |
| 8 | 8 | long |
| 8 | 8 | double |
| 16 | 16 | real |
| 16 | 8 | string |
| 8 | 8 | int[int] |
| 8 | 8 | int* |
| 1 | 1 | EmptyStruct |
| 16 | 8 | Struct |
| 16 | 8 | EmptyClass |
| 17 | 8 | Class |
後で、変数を特定のメモリ位置に構築(配置)する方法について説明する。正確性と効率のため、オブジェクトはアライメントと一致するアドレスに構築する必要がある。
上記のClass型の2つの連続したオブジェクト(それぞれ17バイト)を考えてみよう。0はほとんどのプラットフォームでは変数にとって有効なアドレスではないが、例を簡単にするために、最初のオブジェクトはアドレス0にあると仮定しよう。このオブジェクトの17バイトは、0から16までのアドレスにある。
0 1 16 ┌────┬────┬─ ... ─┬────┬─ ... │<────first object────>│ └────┴────┴─ ... ─┴────┴─ ...
次に使用可能なアドレスは17だが、17はその型の整列値8の倍数ではないため、Classオブジェクトには使用できない。2番目のオブジェクトに最も近いアドレスは24である。24は8の次の最小の倍数だからだ。2番目のオブジェクトをそのアドレスに配置すると、2つのオブジェクトの間に未使用のバイトが発生する。これらのバイトはパディングバイトと呼ばれる。
0 1 16 17 23 24 25 30 ┌────┬────┬─ ... ─┬────┬────┬─ ... ─┬────┬────┬────┬─ ... ─┬────┬─ ... │<────first object────>│<────padding────>│<───second object────>│ └────┴────┴─ ... ─┴────┴────┴─ ... ─┴────┴────┴────┴─ ... ─┴────┴─ ...
次の式で、オブジェクトを配置できる最も近いアドレス値を決定することができる。
この式を使用するには、除算の結果の小数部分は切り捨てられる必要がある。整数型では切り捨ては自動的に行われるため、上記の変数はすべて整数型であると仮定する。
以下の例では、次の関数を使用する。
この関数テンプレートは、テンプレートパラメータからオブジェクトの型を推測する。型がvoid*の場合、これは不可能なので、void*のオーバーロードに明示的なテンプレート引数として型を指定する必要がある。このオーバーロードは、上記の関数テンプレートの呼び出しを簡単に転送できる。
上記の関数テンプレートは、emplace()によってクラスオブジェクトを構築する際に、以下で役立つ。
その型の2つのオブジェクトの間に配置しなければならないパディングバイトを含む、オブジェクトの合計サイズを計算する関数テンプレートをもう1つ定義しよう。
.offsetofプロパティ
アライメントは、ユーザー定義型のメンバーにも適用される。メンバーは、それぞれの型に応じてアライメントされるように、メンバー間にパディングバイトが挿入される場合がある。そのため、次のstructのサイズは、予想通り6バイトではなく12バイトになる。
これは、intメンバーが4の倍数となるアドレスにアラインメントされるように、その前にパディングバイトが挿入されているためだからだ。また、structオブジェクト全体のアラインメントのため、末尾にもパディングバイトが挿入されている。
.offsetofプロパティは、メンバー変数が、その変数が属するオブジェクトの先頭から何バイト目にあるかを示す。次の関数は、.offsetofによってパディングバイトを決定し、型のレイアウトを出力する。
次のプログラムは、上記で定義された12バイトのstruct Aのレイアウトを出力する:
プログラムの出力は、オブジェクト内の6つのパディングバイトの合計の位置を示している。出力の最初の列は、オブジェクトの先頭からのオフセットだ:
| 0 | byte b |
| 1 | ... 3バイトのパディング |
| 4 | int i |
| 8 | ubyte u |
| 9 | ... 3バイトのパディング |
パディングを最小限に抑える1つの手法は、メンバーを大きいものから小さいものの順に並べ替えることだ。例えば、intメンバーを前のstructの先頭に移動すると、オブジェクトのサイズは小さくなる。
この場合、オブジェクトのサイズは末尾の2バイトのパディングのみのため、8に減少する:
| 0 | int i |
| 4 | byte b |
| 5 | ubyte u |
| 6 | ... 2バイトのパディング |
align属性
align属性は、変数、ユーザー定義型、およびユーザー定義型のメンバーの位置合わせを指定するためのものだ。括弧内に指定した値は、位置合わせの値を指定する。すべての定義は個別に指定することができる。例えば、次の定義では、Sオブジェクトは2バイト境界に、そのiメンバーは1バイト境界に位置合わせされる(1バイトの位置合わせでは、パディングはまったく行われない)。
intメンバーが1バイト境界にアラインメントされると、その前にパディングは存在せず、この場合オブジェクトのサイズは正確に6になる:
| 0 | byte b |
| 1 | int i |
| 5 | ubyte u |
alignはユーザー定義型のサイズを縮小できるが、型のデフォルトの整列が守られていない場合、パフォーマンスが大幅に低下する可能性がある(一部のCPUでは、整列されていないデータを使用すると、プログラムが実際にクラッシュする場合もある)。
align変数のアラインメントも指定できる:
ただし、newによって割り当てられたオブジェクトは、GCが想定しているように、size_t型のサイズの倍数に必ず整列する必要がある。そうしないと、未定義の動作になる。例えば、size_tの長さが8バイトの場合、newによって割り当てられた変数の整列は8の倍数でなければならない。
特定のメモリ位置に変数を構築する
- オブジェクトに十分な大きさのメモリを割り当てる。新しく割り当てられたメモリ領域は、型やオブジェクトに関連付けられていない生のメモリ領域と見なされる。
- その型に対応する
.initの値をそのメモリ領域にコピーし、その領域でオブジェクトのコンストラクタを実行する。このステップが完了して初めて、オブジェクトはそのメモリ領域に配置される。 - メモリブロックを構成し、オブジェクトが解放された際に適切に破棄されるための必要なフラグとインフラストラクチャをすべて設定する。
これらのタスクの最初のものは、GC.callocなどのメモリ割り当て関数によって明示的に実現できることはすでに説明した。Dはシステム言語であるため、プログラマは2番目のステップも管理することができる。
std.conv.emplaceを使用して、変数を特定の位置に構築することができる。
特定の場所に構造体オブジェクトを構築する
emplace()は、最初のパラメーターとしてメモリ位置のアドレスをとり、その位置にオブジェクトを構築する。パラメーターが指定された場合、残りのパラメーターをオブジェクトのコンストラクター引数として使用する:
structオブジェクトを構築する場合、オブジェクトの型を明示的に指定する必要はない。emplace()は、ポインタの型からオブジェクトの型を推測するからだ。例えば、次のポインタの型はStudent*であるため、emplace()はそのアドレスにStudentオブジェクトを構築する。
次のプログラムは、3つのオブジェクトを格納するのに十分なメモリ領域を割り当て、そのメモリ領域内のアラインメントされたアドレスに1つずつオブジェクトを構築する。
プログラムの出力:
| Student.sizeof | 0x18 (24)バイト |
|---|---|
| Student.alignof | 0x8 (8)バイト |
| オブジェクト0のアドレス | 7F1532861F00 |
| オブジェクト1のアドレス | 7F1532861F18 |
| オブジェクト2のアドレス | 7F1532861F30 |
[Amy(100), Tim(101), Joe(102)]
特定の位置にクラスオブジェクトを構築する
クラス変数は、クラスオブジェクトとまったく同じ型である必要はない。例えば、型Animalのクラス変数は、Catオブジェクトを参照することができる。このため、emplace()は、メモリポインタの型からオブジェクトの型を決定しない。その代わりに、オブジェクトの実際の型を、emplace()のテンプレート引数として明示的に指定する必要がある。(注釈:さらに、クラスポインタは、クラスオブジェクトではなく、クラス変数へのポインタだ。そのため、実際の型を指定することで、プログラマはクラスオブジェクトを配置するか、クラス変数を配置するかを指定することができる。)
クラスオブジェクトのメモリ位置は、次の構文でvoid[]スライスとして指定する必要がある。
emplace() 指定されたスライス位置にクラスオブジェクトを構築し、そのオブジェクトのクラス変数を返す。
Animal 階層オブジェクトに対してemplace()を使用しよう。この階層オブジェクトは、GC.callocによって割り当てられたメモリ領域に並べて配置される。例をもっと面白くするために、サブクラスのサイズを異なるものにする。これは、前のオブジェクトのサイズに応じて、次のオブジェクトのアドレスを決定する方法を示すのに役立つ。
オブジェクトを格納するバッファは、GC.callocで割り当てられる。
通常、オブジェクト用の容量が常に確保されていることを確認する必要がある。ここでは、例を単純にするためにそのチェックは無視し、例のオブジェクトは 1 万バイトに収まるものと仮定する。
バッファは、CatとParrotオブジェクトの構築に使用される:
Parrotのコンストラクタ引数は、オブジェクトのアドレスの後に指定されることに注意。
emplace()が返す変数は、後でforeachループで使用するためにAnimalスライスに格納される:
詳細な説明はコードのコメント内に記載されている。
出力:
| Catのアドレス | 7F0E343A2000 |
|---|---|
| Parrotのアドレス | 7F0E343A2018 |
meow
squawk, arrgh
main()内の手順をオブジェクトごとに繰り返す代わりに、newObject(T)のような関数テンプレートを使用するとより便利だ。
オブジェクトの明示的な破棄
new演算子の逆操作は、オブジェクトを破棄し、オブジェクトのメモリをGCに返すことだ。通常、これらの操作は指定されていないタイミングで自動的に実行される。
しかし、プログラム内の特定の時点でデストラクタを実行する必要がある場合もある。例えば、オブジェクトがデストラクタ内でFileメンバーを閉じている場合、そのオブジェクトのライフタイムが終了したらすぐにデストラクタを実行しなければならない。
デストラクタの実行後、destroy()は変数をその.init状態に設定する。クラス変数の.init状態はnullであるため、クラス変数は一度破棄されると使用できなくなることに注意しよう。destroy()は、デストラクタを実行するだけである。破棄されたオブジェクトが占めていたメモリをいつ再利用するかは、GCに依存する。
警告: 構造体ポインタで使用する場合、destroy()はポインタではなく、ポインタが指すオブジェクトを受け取る必要がある。そうしないと、ポインタはnullに設定されるが、オブジェクトは破棄されない。
destroy()がポインタを受け取った場合、破壊されるのはポインタそのもの(つまり、ポインタがnullになる):
値42でオブジェクトを構築中
destroy()の前
destroy()の後 ← この行の前にオブジェクトが破壊されていない
p: null ← 代わりにポインタがnullになる
mainを離れる
値42のオブジェクトを破壊中
そのため、構造体ポインタで使用する場合、destroy()はポインタではなく、ポインタが指すオブジェクトを受け取る必要がある。
この場合、デストラクタは正しい場所で実行され、ポインタはnullに設定されない:
値42のオブジェクトを構築中
destroy()の前
値42のオブジェクトを破壊中 ← 正しい位置で破壊された
destroy()の後
p: 7FB64FE3F200 ← ポインタはnullではない
mainを離れる
値0のオブジェクトを破棄中 ← S.initのために再度
最後の行は、同じオブジェクトに対してデストラクタがもう一度実行されたため。このオブジェクトの値は、S.initになっている。
実行時に名前でオブジェクトを構築する
Objectのメンバ関数factory()は、クラス型の完全修飾名をパラメータとして受け取り、その型のオブジェクトを構築し、そのオブジェクトのクラス変数を返す。
このプログラムには明示的なnew式は含まれていないが、3つのクラスオブジェクトが作成され、animalsスライスに追加されている。
pind.samples.ja.memory.memory_10.Catを構築中
pind.samples.ja.memory.memory_10.Dogを構築中
pind.samples.ja.memory.memory_10.Catを構築中
ニャー
ワン
ニャー
Object.factory()は、オブジェクトの型の完全修飾名を受け取ることに注意。また、factory()の戻り値の型はObjectなので、プログラムで使用する前に、オブジェクトの実際の型にキャストする必要がある。
要約
- ガベージコレクタは、指定されていないタイミングでメモリをスキャンし、プログラムから到達できなくなったオブジェクトを特定し、それらを破棄してメモリ領域を解放する。
- GCの動作は、
GC.collect、GC.disable、GC.enable、GC.minimizeなどによって、プログラマーが一定程度制御できる。 GC.callocはメモリを予約し、GC.reallocは以前に割り当てられたメモリ領域を拡張し、GC.freeはそれをGCに返す。GC.BlkAttr.NO_SCAN、GC.BlkAttr.NO_INTERIORなどの属性で割り当てられたメモリをマークすることができる。.alignofプロパティは、型のデフォルトのメモリアラインメントだ。クラスオブジェクトの整列は、classInstanceAlignmentによって取得する必要がある。.offsetofプロパティは、メンバーが属するオブジェクトの先頭からのメンバーの位置のバイト数だ。align属性は、変数、ユーザー定義型、またはメンバーのアライメントを指定する。emplace()structオブジェクトを構築する際にはポインタを、classオブジェクトを構築する際にはvoid[]スライスを受け取る。destroy()オブジェクトのデストラクタを実行する。(構造体ポインタではなく、構造体ポインタが指す構造体を破棄する必要がある。)Object.factory()完全修飾型名を持つオブジェクトを構築する。