Javaファイナライズのメモリ保持問題への対処方法はじめにJavaのファイナライズ機能は、ガベージコレクタが到達不能と判断したオブジェクトに対して事後クリーンアップを実行するための仕組みです。通常は、オブジェクトに関連付けられたネイティブリソースを再生(reclaim)する場合に使います。簡単なファイナライズの例を次に示します。 public class Image1 { // pointer to the native image data private int nativeImg; private Point pos; private Dimension dim; // it disposes of the native image; // successive calls to it will be ignored private native void disposeNative(); public void dispose() { disposeNative(); } protected void finalize() { dispose(); } static private Image1 randomImg; } ここで、ファイナライズ可能なオブジェクト(クラスに有効なファイナライザが定義されているオブジェクト)が作成されてから再生されるまでの流れを見てみましょう。オブジェクトの名前は
ガベージコレクタが サブクラス作成時のメモリ保持問題を回避するファイナライズを明示的に使っていない場合でも、リソースの再生に遅延が生じる可能性があります。次に例を示します。 public class RGBImage1 extends Image1 { private byte rgbData[]; } この問題を回避する1つの方法は、次のように、「継承」パターンではなく、「包含」パターンを使うようにコードを修正することです。 public class RGBImage2 { private Image1 img; private byte rgbData[]; public void dispose() { img.dispose(); } } しかし、必ずしも前述のようにコードを修正できるとは限りません。修正できない場合は、クラスのユーザーとして、インスタンスがファイナライズ時に必要以上のリソースを消費しないようにもう少し工夫する必要があります。次のコードを参照してください。 public class RGBImage3 extends Image1 { private byte rgbData[]; public void dispose() { super.dispose(); rgbData = null; } } メモリ保持問題からユーザーを解放する前節では、ファイナライザを使うサードパーティ製クラスを利用するときのメモリ保持問題の回避方法について説明しました。今度は、自分でクラスを開発する場合を想定し、ユーザーにこのような手間をかけさせないために、事後クリーンアップを要求するクラスの作成方法を考えてみましょう。一番よい方法は、クラスを2つ(事後クリーンアップの必要なデータを格納するクラスと、それ以外のデータを格納するクラス)に分け、前者に対してのみファイナライザを定義する方法です。具体的な例を次に示します。 final class NativeImage2 { // 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(); } protected void finalize() { dispose(); } } public class Image2 { private NativeImage2 nativeImg; private Point pos; private Dimension dim; public void dispose() { nativeImg.dispose(); } } 細かいことを言えば、 ファイナライズの代替手段前節の例では、まだ不確定要素が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(); } } 参照オブジェクトの参照対象がガベージコレクタによって再生処理されたかどうかを判断する手段としては、明示的な方法(参照オブジェクトの 参照オブジェクトはガベージコレクタによってのみ検出され、到達可能な場合のみ参照キューに追加されることに注意してください。それ以外の場合は、他の到達不能なオブジェクトと同様に再生処理されます。すべての JVMにより、
ReferenceQueue<Image3> refQueue =
NativeImage3.referenceQueue();
while (true) {
NativeImage3 nativeImg =
(NativeImage3) refQueue.remove();
nativeImg.dispose();
}
これは極めて単純な例です。経験豊富な開発者であれば、解放させたい優先順序を考慮して、さまざまな参照オブジェクトをそれぞれの参照キューに関連付けることもできるでしょう。1つの「クリーンアップ」スレッドで、利用可能なすべての参照キューをポーリングし、必要な優先順位に従ってオブジェクトをキューから取り出すことも可能です。さらに、アプリケーションにあまり影響を与えないように、リソースの再生処理を分散させることもできます。 このようなクリーンアップ処理はファイナライズを使うよりも複雑なプロセスですが、強力で柔軟性に優れており、ファイナライズに伴う不確定要素を最小限に抑えます。また、この仕組みは、JVMにおけるファイナライズの実装方法によく似ています。大量のネイティブリソースを明示的に使い、クリーンアップ時に細かい制御を必要とするプロジェクトでは、このアプローチをお勧めします。 注
本稿では、ファイナライズの使用時に発生する2種類の問題、つまりメモリ保持問題とリソース保持問題だけを取り上げました。ファイナライズの使用と
Referenceクラスが原因で、非常に微妙な同期問題が発生することもあります。詳しくは、『Finalization, Threads, and the Java Technology-Based Memory Model』(Hans-J. Boehm著)を参照してください。ファイナライズは必要なときだけ使う本稿では、ファイナライズがJVMでどのように実装されているかを簡単に説明しました。ファイナライズ可能なオブジェクトによってメモリが不必要に保持される場合の例を示し、このような問題の解決策を紹介しました。最後に、ファイナライズの代わりに、柔軟性が高く予測可能な方法で事後クリーンアップを実行できる弱参照の利用方法を説明しました。 ただし、不足する可能性のあるネイティブリソースを再生するという目的のために、ガベージコレクタに全面的に頼って到達不能なオブジェクトを特定するというやり方には重大な欠陥があります。大量のメモリを使ってわずかばかりのリソースを保護しても、あまり意味はありません。そのため、ネイティブリソースが関連付けられているオブジェクト(GUIコンポーネント、ファイル、ソケットなど)を使う場合は、終了時に必ず また、ファイナライズの使用は、絶対に必要な場合だけにしてください。ファイナライズは、不確定的であるだけでなく、場合によっては予測不可能なプロセスです。利用する回数が少なければ少ないほど、JVMとアプリケーションに与える影響も小さくなります。 謝辞本稿に対して数々の建設的意見を寄せてくれたPeter Kesslerに感謝します。 商標Javaは、Sun Microsystems, Inc.の米国およびその他の国における商標または登録商標です。 著者紹介Tony Printezis(Tony Printezis)
SunLabsでの3年以上の勤務を経て、Sun MicrosystemsのJava HotSpot Virtual Machine開発チームに参加。主に、ガベージコレクタのスケーラビリティ、反応性、並列性、ビジュアル化を中心に、動的メモリ管理に携わる。
|