ファイナライズの代替手段
前節の例では、まだ不確定要素が1つあります。JVMは、ファイナライズキュー内にあるオブジェクトのファイナライザを呼び出す順序を保証しません。すべてのクラス(アプリケーション、ライブラリなど)のファイナライザは平等に処理されます。そのため、ファイナライザの処理速度が遅いオブジェクトがあると、それが原因でファイナライズキューに渋滞が発生し、大量のメモリを消費しているオブジェクトや、なけなしのリソースを抱え込んでいるオブジェクトがなかなか回収されないという事態が起こり得ます。
この種の不確定要素を回避するため、ファイナライズの代わりに弱参照(weak reference)を事後処理に使うことができます。こうすれば、JVMに再生処理の順序を任せるのではなく、再生処理の優先順位を完全に制御することができます。次に、この方法の例を示します。
final class NativeImage3 extends WeakReference<Image3> { // pointer to the native image data private int nativeImg; // it disposes of the native image; // successive calls to it will be ignored private native void disposeNative(); void dispose() { disposeNative(); refList.remove(this); } static private ReferenceQueue<Image3> refQueue; static private List<NativeImage3> refList; static ReferenceQueue<Image3> referenceQueue() { return refQueue; } NativeImage3(Image3 img) { super(img, refQueue); refList.add(this); } } public class Image3 { private NativeImage3 nativeImg; private Point pos; private Dimension dim; public void dispose() { nativeImg.dispose(); } }
Image3はImage2と同じです。NativeImage3は、事後クリーンアップがファイナライズではなく弱参照を利用している点以外はNativeImage2と同じです。NativeImage3はWeakReferenceの継承クラスで、参照対象はImage3インスタンスです。参照オブジェクト(この場合はWeakReference)の参照対象が到達不能になると、参照オブジェクトは所定の参照キューに追加されます。今回の例ではnativeImgを参照オブジェクトに埋め込んでいるため、JVMは必要なものだけを参照キューに追加します(図7を参照)。繰り返しますが、前述の理由のとおり、NativeImage3をImage3の内部クラスにしてはなりません。
参照オブジェクトの参照対象がガベージコレクタによって再生処理されたかどうかを判断する手段としては、明示的な方法(参照オブジェクトのget()メソッドを呼び出す)と、暗黙的な方法(参照オブジェクトが参照キューに入っていることをもって判断する)の2つがあります。この例では、暗黙的な方法だけを使います。
参照オブジェクトはガベージコレクタによってのみ検出され、到達可能な場合のみ参照キューに追加されることに注意してください。それ以外の場合は、他の到達不能なオブジェクトと同様に再生処理されます。すべてのNativeImage3インスタンスを静的リストに追加するのはそのためです(実際には、どのようなデータ構造体でも構いません)。これにより、インスタンスを到達可能なまま残し、参照対象が到達不能になったら処理されるようにします。当然ですが、解放する場合はリストから確実に削除することも必要です(これはdispose()メソッドで実行されます)。
Image3インスタンスのdispose()メソッドが明示的に呼び出された場合、それ以降、そのインスタンスで事後クリーンアップは発生しません。クリーンアップの必要がないからです。このdispose()メソッドは、静的リストからNativeImage3インスタンスを削除します。Image3インスタンスが到達不能になると、NativeImage3インスタンスも到達不能になるからです。そして、前述したとおり、到達不能な参照オブジェクトは参照キューに追加されません(一方、前述のファイナライズを使用した例では、ファイナライズ可能なオブジェクトは、関連付けられているネイティブリソースを明示的に解放したかどうかに関係なく、到達不能になると常にファイナライズ対象と見なされます)。
JVMにより、Image3インスタンスがガベージコレクタから到達不能と見なされると、対応するNativeImage3インスタンスは参照キューに追加されます。そのインスタンスをキューから取り出し、対応するネイティブリソースを解放する処理は開発者に任されています。この処理は、いわゆる「クリーンアップ」スレッドで実行される、次のようなループで実行できます。
ReferenceQueue<Image3> refQueue =
NativeImage3.referenceQueue();
while (true) {
NativeImage3 nativeImg =
(NativeImage3) refQueue.remove();
nativeImg.dispose();
}
これは極めて単純な例です。経験豊富な開発者であれば、解放させたい優先順序を考慮して、さまざまな参照オブジェクトをそれぞれの参照キューに関連付けることもできるでしょう。1つの「クリーンアップ」スレッドで、利用可能なすべての参照キューをポーリングし、必要な優先順位に従ってオブジェクトをキューから取り出すことも可能です。さらに、アプリケーションにあまり影響を与えないように、リソースの再生処理を分散させることもできます。
このようなクリーンアップ処理はファイナライズを使うよりも複雑なプロセスですが、強力で柔軟性に優れており、ファイナライズに伴う不確定要素を最小限に抑えます。また、この仕組みは、JVMにおけるファイナライズの実装方法によく似ています。大量のネイティブリソースを明示的に使い、クリーンアップ時に細かい制御を必要とするプロジェクトでは、このアプローチをお勧めします。
Referenceクラスが原因で、非常に微妙な同期問題が発生することもあります。詳しくは、『Finalization, Threads, and the Java Technology-Based Memory Model』(Hans-J. Boehm著)を参照してください。ファイナライズは必要なときだけ使う
本稿では、ファイナライズがJVMでどのように実装されているかを簡単に説明しました。ファイナライズ可能なオブジェクトによってメモリが不必要に保持される場合の例を示し、このような問題の解決策を紹介しました。最後に、ファイナライズの代わりに、柔軟性が高く予測可能な方法で事後クリーンアップを実行できる弱参照の利用方法を説明しました。
ただし、不足する可能性のあるネイティブリソースを再生するという目的のために、ガベージコレクタに全面的に頼って到達不能なオブジェクトを特定するというやり方には重大な欠陥があります。大量のメモリを使ってわずかばかりのリソースを保護しても、あまり意味はありません。そのため、ネイティブリソースが関連付けられているオブジェクト(GUIコンポーネント、ファイル、ソケットなど)を使う場合は、終了時に必ずdispose()または同等のメソッドを呼び出すようにします。これで、ネイティブリソースの再生が速やかに行われ、リソース不足の可能性が軽減されます。本稿で紹介した事後クリーンアップのアプローチは、中心的なクリーンアップ手段ではなく、最後の手段として使用してください。
また、ファイナライズの使用は、絶対に必要な場合だけにしてください。ファイナライズは、不確定的であるだけでなく、場合によっては予測不可能なプロセスです。利用する回数が少なければ少ないほど、JVMとアプリケーションに与える影響も小さくなります。
謝辞
本稿に対して数々の建設的意見を寄せてくれたPeter Kesslerに感謝します。
商標
Javaは、Sun Microsystems, Inc.の米国およびその他の国における商標または登録商標です。

