【Effective Java】項目7:ファイナライザを避ける

Java のファイナライザは予想不可能で、危険であり、一般的には使う必要はない。

C++ プログラマJava のファイナライザを C++ のデストラクタと対応付けて考えてしまいがちだが、それは大きな間違いである。

C++ のデストラクタはコンストラクタに対応するものであり、オブジェクトに関連付けられたリソースを回収するための唯一の方法である。 一方、Java ではオブジェクトが到達不可能になったとき、関連付けられたリソースはガーベージコレクタ(GC)が自動で回収する。

C++ デストラクタはメモリ以外の他のリソースの回収にも利用されることもあるが、Java ではこれに対応するイディオムとして try - finally を利用する方が一般的である。

ファイナライザの欠点

ファイナライザの欠点の1つはオブジェクトが到達不可能になっても、即座には呼ばれないことである。 これは時間的に制約がある解放処理をファイナライザでやるべきではないことを示している。

ファイナライザの呼び出しはガーベージコレクションの機能であるが、この実装方法は個々の JVM 実装によって大きく異なっている。 すなわち、ファイナライザがどのタイミングで呼ばれるかは実装依存である。

即時性がないということは、プログラムが終了するまでにファイナライザが呼ばれない可能性もある。 そのため、重要な永続性の状態を更新するためにファイナライザに頼ってはならない(例:データベースの共有ロックの解除など)。

System.gc や System.runFinalization はファイナライザの呼び出しを保証していない。 一方、System.runFinalizersOnExit と Runtime.runFinalizersOnExist はファイナライザの呼び出しを保証するが、これらのメソッドは一般的に動作が不安定な評判の悪いメソッドである。

また、ファイナライザを追加するとパフォーマンスに影響を与える。 著者の環境では、ファイナライザを追加した場合、オブジェクトの生成と解放が 5.6 nsec から 2400 nsec に大幅に悪化した。

明示的終了メソッド

ファイナライザに頼らずリソースを解放するために、クライアントには明示的終了メソッドを提供するべきである。

明示的終了メソッドは、呼び出しを確実にするために、try-finally パターンと合わせて用いられる。

Foo foo = new Foo();
try {
    ...
} finally {
    foo.terminate();
}

細かな実装の注意点としては、terminate メソッドは一度だけ呼ばれることを確認するべきである。 二度目に呼ばれた場合は IllegalStateException などをスローするべき。

ファイナライザが有効な場合

ファイナライザは一般的に不要な機能であるが、有効に利用できる場合がいくつかある。

一つ目は安全ネットとしてのファイナライザ。 クライアントが明示的終了メソッドを忘れたときのために、安全ネットとしてリソースの解放処理を書いておく。 ただし、ファイナライザが呼ばれていること、すなわち明示的終了メソッドが呼ばれていないことを示す警告ログを出すようにするべきである。

Java ライブラリの InputStream や OutputStream ではこの安全ネットとしてのファイナライザが利用されている(ただし、上述した警告はでるようになっていないので注意が必要)。

二つ目はネイティブピアと呼ばれる、通常のオブジェクトがネイティブメソッドを通して処理を行うネイティブオブジェクトに対しての利用である。 これらのネイティブピアは直接 GC の対象とならないため、ネイティブピアの解放にファイナライザが利用できる。

ただし、この場合もファイナライザには即時性がないため、明示的終了メソッドを持つほうがよりよい方法である。

ファイナライズ連鎖とファイナライザガーディアン

クラス階層に対し、ファイナライズの連鎖は自動で行われないため、手動でファイナライザ連鎖を実行する必要がある

@Override
protected void finalize() throws Throwable {
    try {
        ....
    } finally {
        super.finalize();
    }
}

継承される可能性のあるクラスでファイナライズを用いる場合、ファイナライザガーディアンと呼ばれる無名クラスを検討したほうがよい。

public class Foo {
    private final Object finalizeGurdian = new Object() {
        @Override
        protected void finalize() throws Throwable {
            // 外側の Foo をオブジェクトをファイナライズする
        } 
    }
}

このようにすると Foo のインスタンスが破棄される場合、finalizeGurdian オブジェクトが破棄されるため、必ず finalizerGuradian の finalize が呼ばれる。

感想

Java のファイナライザは使うな」くらいの認識しかなかったので、ファイナライザの即時性とか勉強になった。

ファイナライズキューって JVM の中に実装されてる(すなわち、Java としては意識できない)かと思ってたんだけど、会社で作ってる Andoroid アプリのクラッシュレポート見たときに、スレッドの一つに finalizeQueue スレッドみたいなのがあって、実行時にシステムがスレッド立ててそれがファイナライザを呼ぶのかなーと思ってきた。というかたぶんそうだな。

ここらへんは GC仕様書をちゃんと読めば分かるだろう。数ヶ月後の自分、がんばれ!