多くのオブジェクト指向言語では、オブジェクトが解放された時に動く処理を登録できます。そして、その実装には「デストラクタ」と「ファイナライザ」がありますが、決定的に違いがあります。
メモリ解放の仕組み
new
で確保したものに対して明示的にdelete
を呼ばないといけないC++であれば解放のタイミングは明らかですが、そうでない言語では、プログラマの見えないところでガベージコレクタという、メモリを解放するための仕組みが動いています。そして、このガベージコレクタには、大きく分けて2通りのタイプがあります(掘り下げていけばそれだけで1冊の本になるので、軽く触れる程度にします)。
- 参照カウント…オブジェクトへの参照の数をカウントしておいて、0になった瞬間に解放する方法
- マークアンドスイープ…ときどきどの変数が有効かをチェックして、参照できなくなっているものを解放する方法
この2つのシステムどちらを取るかで、後始末処理の実行形態も大きく変わってきます。
デストラクタ
参照カウントのシステムの場合(例:PHP)、あるいはC++のように自分から解放しないといけない場合、最後の参照がスコープアウトした、あるいは別な値に書き潰されたなどで参照不能となった時点で、後始末処理のデストラクタが走り始めます。つまり、むやみに参照を拡散させずにローカルにしておけば、デストラクタは確実に実行されます。
これの利用法として、「コンストラクタであるリソースを確保して、デストラクタでそれを解放する」クラスを用意しておけば、あるスコープにローカルな形でそのクラスのインスタンスを持つことで
- ふつうにスコープを抜けた場合
- 例外を投げて飛んでいった場合
-
break
やreturn
で脱出する場合
など、どうコードが進もうともスコープアウトに連動したデストラクタでリソースは解放されることとなります。これをRAIIといいます。
ファイナライザ
これに対して、マークアンドスイープを行う場合、参照不能となったオブジェクトもガベージコレクタが回ってくるまではメモリに残り続け、そしてガベージコレクタが参照不能だと判断してから後始末処理が始まります。Javaではこれをファイナライザと呼んでいます。ややこしいことに、C#では同様の動作をするものを「デストラクタ」と呼んでいますが、動作の確実性はないのでこちらに区分されます。
上に書いたように、ガベージコレクタはいつ回ってくるかわかりませんので、参照不能となった後にいつファイナライザが動き出すかはプログラマの制御できない話です。それどころか、ファイナライザを実行しないままプログラムが終了してしまったとしても、(少なくともJavaでは)それも仕様の範囲内の動作です。そして、仮にファイナライザで例外が起きたとしてもそれは闇に葬られてしまいます。
つまり、デストラクタのように「何かを積極的に行う」1場所としてはファイナライザは使いえないものなので、せいぜい「うっかり開いたままだったリソースを閉じる」というようなフェイルセーフに使うのが精一杯でしょう。
リソースの自動解放
なお、コンストラクタ - デストラクタによるRAIIはファイナライザでは書くことができませんが、言語ごとに(どんな形で抜けても)リソースをきっちり閉じるような構文があります。
- C#…
IDisposable
インタフェースのDispose()
を呼び出すusing
構文 - Java…
java.io.Closable
によるtry with resource - Ruby…ブロック内でのみ有効なリソースを持つ、ブロック付きメソッド
外部リンク
-
コンストラクタが途中で失敗した場合にもファイナライザに飛ぶので、そのオブジェクトへの参照を不正に得るためにファイナライザを「使えてしまう」事例はあります。 ↩