読者です 読者をやめる 読者になる 読者になる

Effective Java 第 2 版:第 2 章 オブジェクトの生成と消滅 項目 7

Java 読書メモ

ファイナライザを避ける

Effective Java 第 2 版、第 2 章の項目 7。

ファイナライザはオブジェクトがガベージコレクション (GC) により破棄されるときに呼び出されるメソッドである。 Java では Object クラスで protected void finalize() として定義されている(中身は空)。 実際には、finalize をオーバーライドして使う。

ファイナライザはオブジェクトの持つ資源(メモリ、ファイル)を解放するために使えそうである。 しかし、この項目は、ほとんどの場合でファイナライザは使うべきでないと述べられている。

なぜ使うべきでないか

ファイナライザを使うべきでない理由は以下の通りである。

  1. ファイナライザがいつ実行されるかわからない
  2. 不正にファイナライザが終了する危険性がある
  3. ファイナライザは非常にコストが高い

1 は、ファイナライザが GC に伴うものであることによる。 GCアルゴリズムJVM 実装依存であることにより、プログラマの意図したタイミングでファイナライズされない。 また、GC の対象オブジェクトもファイナライズされる保証はない。

2 は、ファイナライズ中に例外が投げられると起きる。 これにより、オブジェクトが不正な状態のままになる可能性がある。 さらに、ファイナライザで投げられた例外はスタックトレースを表示してプログラムを終了させない。

3 については、ファイナライザを持つオブジェクトの生成と解放は、それを持たないオブジェクトの 430 倍遅いということである。

ファイナライザの代替策:明示的終了メソッド

ファイナライザに代わる方法として、明示的終了メソッド (explicit termination method) が提案されている。 例えば terminate というメソッドの中で資源を後始末する処理を書いておき、以下のように使えばよい。

Hoge obj = new Hoge();
try {
    obj.doSomething();
    //...
} catch {
    obj.terminate();    // ファイナライザの代わりに明示的終了メソッド
}

あるオブジェクトの明示的終了メソッドが呼び出されたあとに、そのオブジェクトが使われないようにする必要がある。 そのためには、private フィールドにそのオブジェクトが終了済みであることを記録し、終了済みのオブジェクトが利用されれば IllegalStateException が投げられるようにすればよい。

ファイナライザの存在意義

では、ファイナライズの存在意義は何になるのだろうか。 それは以下の二つである。

  1. セーフネット(安全ネット)
  2. ネイティブピアの回収

1 は、明示的終了メソッドを呼び忘れた際のセーフネットとして使えるということである。 このセーフネットが使われたときは、何らかの方法で正しく使われていないことに対する警告を記録しておくべきである。

2 は、JVM 上ではなく CPU 上で動いているネイティブオブジェクトに関係する。 ネイティブピアは、通常のオブジェクトがネイティブメソッドを通して委譲するネイティブオブジェクトのことである。 ネイティブピアは JVM のあずかり知るところではないので、GC で解放されない。 そのため、ネイティブピアが重要な資源を持っていないときはファイナライザで解放するのが適切である。 しかし、即座に解放したい資源をネイティブピアが持っているときは、明示的終了メソッドで解放したほうがよい。

ファイナライザガーディアン

ファイナライザを持つクラスがサブクラスを持つ場合を考える。 このサブクラスで finalize をオーバーライドすると、その finalize 内で super.finalize を呼ばないと、スーパークラスファイナライズされない。 つまり、ファイナライザはサブクラスからスーパークラスへ連鎖的に呼び出しされない。 super.finalize を呼ぶために、以下のようにすればよい。

@Override protected void finalize() throws Throwable {
    try {
        // ファイナライズする
    } finally {
        super.finalize();
    }
}

しかし、finally 節を忘れるとスーパークラスファイナライズされない。 これを防ぐために、ファイナライザガーディアンを使うことができる。 ファイナライザガーディアンは、ファイナライズしたいクラスの内部クラスとして、無名クラスを使い、以下のように書く。

public class Hoge {
    private final Object finalizerGurdian = new Object() {
        @Override protected void finalize() {
            // Hoge をファイナライズする処理
        }
    };
    // ...
}

例えば、Hoge のサブクラス Fuga を考える。

Fuga extends Hoge {
    @Override protected void finalize() {
        // Fuga のファイナライズ処理
        // super.finalize は忘れている
    // ...
}

Fugaインスタンスがどの変数からも参照されない状態になったとき、GC によってファイナライズの対象となるインスタンスは、以下の二つである。

Fugaインスタンスファイナライズされるとき、Fuga のファイナライザ内で super.finalize を忘れていたとしても、ガーディアンのファイナライザにより、そのエンクロージングインスタンスである Hogeインスタンスファイナライズされる。

参考文献

EFFECTIVE JAVA 第2版 (The Java Series)

EFFECTIVE JAVA 第2版 (The Java Series)