サブクラス作成時のメモリ保持問題を回避する
ファイナライズを明示的に使っていない場合でも、リソースの再生に遅延が生じる可能性があります。次に例を示します。
public class RGBImage1 extends Image1 { private byte rgbData[]; }
RGBImage1はImage1の継承クラスで、フィールドrgbData(およびこの例では示されていない一部のメソッド)を継承します。RGBImage1でファイナライザを明示的に定義しなかったとしても、このクラスはImage1からfinalize()メソッドを自然に継承するので、RGBImage1のインスタンスもすべてファイナライズ可能と見なされます。RGBImage1のインスタンスが到達不能になった場合、このインスタンスに含まれるrgbData配列は、インスタンスがファイナライズされるまで回収されません(図3を参照)。rgbData配列は非常に大きくなることもあるので、この配列の回収が遅れるとリソースの使用効率が低下します。ファイナライザが深いクラス階層の中に「隠されている」場合、この問題の原因を見つけるのはなかなか困難です。
この問題を回避する1つの方法は、次のように、「継承」パターンではなく、「包含」パターンを使うようにコードを修正することです。
public class RGBImage2 { private Image1 img; private byte rgbData[]; public void dispose() { img.dispose(); } }
RGBImage1と異なり、RGBImage2はImage1の継承クラスではなく、Image1のインスタンスを包含しています。RGBImage2のインスタンスが到達不能になると、ガベージコレクタは直ちにRGBImage2のインスタンスとrgbData配列(他のオブジェクトから到達不能になっている場合)を回収し、Image1インスタンスだけをファイナライズキューに追加します(図4を参照)。クラスRGBImage2はImage1のサブクラスではないため、メソッドを継承しません。したがって、RGBImage2には、Image1の必須メソッド(たとえばdispose()メソッド)にアクセスする委譲メソッドを追加する必要があります。
しかし、必ずしも前述のようにコードを修正できるとは限りません。修正できない場合は、クラスのユーザーとして、インスタンスがファイナライズ時に必要以上のリソースを消費しないようにもう少し工夫する必要があります。次のコードを参照してください。
public class RGBImage3 extends Image1 { private byte rgbData[]; public void dispose() { super.dispose(); rgbData = null; } }
RGBImage3は、基本的にはRGBImage1と同じですが、dispose()メソッドが追加されているという点が異なります。このdispose()メソッドでは、rgbDataフィールドをnullにします。RGBImage3インスタンスの使用後にdispose()を明示的に呼び出し、rgbData配列を速やかに回収する必要があります(図5を参照)。フィールドを明示的に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(); } }
Image2は、Image1と似ていますが、nativeImgフィールドを別のクラスNativeImage2内に格納しています。そのため、Image2クラスからnativeImgにアクセスするには、間接的な手段を取る必要があります。しかし、Image2インスタンスが到達不能になった場合は、NativeImage2インスタンスだけがファイナライズキューに追加され、Image2インスタンスから到達可能なものはすべて速やかに回収されます(図6を参照)。クラスNativeImage2はfinalと宣言されているので、ユーザーはNativeImage2のサブクラスを作成できず、前述のメモリ保持問題が再現されることはありません。
細かいことを言えば、NativeImage2はImage2の内部クラスであってはなりません。内部クラスのインスタンスは、作成元である外部クラスのインスタンスの暗黙的参照を持ちます。したがって、NativeImage2がImage2の内部クラスで、NativeImage2インスタンスがファイナライズキューに追加された場合は、対応するImage2インスタンスも保持されることになります。これは、まさに回避しようとしている問題です。しかし、NativeImage2クラスがImage2クラスからのみアクセス可能ならどうでしょうか。NativeImage2クラスにパブリックメソッドを用意していないのはこのためです(クラスだけでなくdispose()メソッドもパッケージ内でのみ利用可能です)。




