2-8:ファイナライザとクリーナーを避ける
結論
finalize()(ファイナライザ)は基本的に使わない。
Cleaner(java.lang.ref.Cleaner)も最終手段に留め、正しい対処は AutoCloseable を実装して明示的に close()(および try-with-resources)を使うこと。
なぜ finalize() を避けるのか
-
実行時期が不確定(ガベージコレクタに依存する)
→ リソース(ファイル、ソケット、ネイティブメモリ)の解放が遅れる。
-
実行される保証がない(VM 終了時は呼ばれないことがある)。
-
パフォーマンス悪化:ファイナライザつきオブジェクトは特殊扱いされ GC コストが上がる。
-
オブジェクトを「生き返らせ(resurrect)」ることができて複雑さとバグを招く。
-
Finalizer thread 上で実行され、同期的にロックを取るとデッドロックの原因になる。
- 例外処理が扱いにくい(例外が抑制されがち)。
悪い例
1. finalize() を使う
public class BadResource {
private long nativeHandle; // ネイティブ資源のハンドル
public BadResource() {
nativeHandle = allocateNative();
}
@Override
protected void finalize() throws Throwable {
// 悪い:遅延・非決定的・例外が無視されうる
super.finalize();
freeNative(nativeHandle);
}
private native long allocateNative();
private native void freeNative(long handle);
}
問題点:
- freeNative はいつ呼ばれるか分からない(メモリ枯渇を招く)
- finalize 内で例外が出ても呼び出し側へ伝播しづらい
- finalize でオブジェクトを復活させるコードが混入するとバグ地獄になる
2. 復活/resurrect
public class Resurrect {
static Resurrect saved;
@Override
protected void finalize() {
// finalize で自分の参照を静的変数に入れてしまう — 一度だけ生き返る
saved = this;
}
}
問題点:
- 予想外のライフサイクルによりメモリ管理が壊れる。
良い例
1. 明示的に close() を提供して try-with-resource を使う (★推奨)
public class GoodResource implements AutoCloseable {
private long nativeHandle;
private boolean closed = false;
public GoodResource() {
nativeHandle = allocateNative();
}
@Override
public void close() {
if (!closed) {
freeNative(nativeHandle);
closed = true;
}
}
// 使用例
public static void main(String[] args) {
try (GoodResource r = new GoodResource()) {
// r を使う
} // close() が自動で呼ばれる
}
private native long allocateNative();
private native void freeNative(long handle);
}
利点
-
リソース解放の時点が明確(すぐに解放可能)
-
例外処理が try-with-resources により簡潔に扱える
-
テストしやすく、動作が予測可能
実装上の注意:
close() は冪等(何度呼んでも安全) に実装すること。
マルチスレッドで使うなら volatile や AtomicBoolean を使ってスレッドセーフにする。
2. Cleaner を「最後の安全網」として使う(非推奨)
Cleaner は finalize() よりは安全で(復活を許さない、別スレッドで実行)、JDK 9+ で提供されているが、依然として解放時期は非決定的なので主たる解放手段にしてはいけない。
あくまで利用者が close() を忘れたときのバックストップとして使うべき。
import java.lang.ref.Cleaner;
import java.util.concurrent.atomic.AtomicBoolean;
public class ResourceWithCleaner implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
// state に解放処理を保持(Runnable)
private static class State implements Runnable {
private long nativeHandle;
State(long handle) { this.nativeHandle = handle; }
@Override
public void run() { freeNative(nativeHandle); }
}
private final State state;
private final Cleaner.Cleanable cleanable;
private final AtomicBoolean closed = new AtomicBoolean(false);
public ResourceWithCleaner() {
long h = allocateNative();
state = new State(h);
cleanable = CLEANER.register(this, state);
}
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
cleanable.clean(); // 明示的にクリーン(即時)
}
}
private static native long allocateNative();
private static native void freeNative(long handle);
}
ポイント:
-
CLEANER.register(this, state) は、this が GC の到達不能になったときに state.run() を実行する。
-
close() で cleanable.clean() を呼んで即時解放できる(try-with-resources を推奨)。
-
Cleaner は finalizer より安全だが 解放のタイミングは GC に依存するので、あくまで「保険」。
まとめ
1. ファイナライザは使わない。ほぼ常に害が勝つ。
2. リソース管理は 明示的な API(close() / AutoCloseable) + try-with-resources で行う。
3. Cleaner は finalizer の代替としてはより安全だが、主手段にしてはいけない。あくまでフォールバック(利用者が close を忘れたときの最後の保険)で登録するに留める。
4. close() は 例外を適切に扱い、冪等に実装する。スレッドセーフにするなら AtomicBoolean 等を使う。
5. 既存の finalize() を持つレガシーコードがあるなら、可能な限り finalize() を取り除き、上のパターンに置き換える。