japan.internet.comThe Internet & IT Network
RSS
  • ニュース
  • コラム
  • リサーチ
  • ヘッドライン
  • 特集
  • ブログ
  • プレスリリース
  • 専門チャンネル
  • イベント
  • ランキング
  • ニュースメール
2008年10月15日
文字サイズ文字サイズ小文字サイズ中文字サイズ大
デベロッパー2006年4月4日 10:00

Javaファイナライズのメモリ保持問題への対処方法

海外海外internet.com発の記事
  • このエントリーを含むはてなブックマーク
  • この記事をクリップ!
  • Buzzurlにブックマーク
  • Yahoo!ブックマークに登録
  • newsing it!

はじめに

 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;
}

 Image1のインスタンスが到達不能になると、Java仮想マシン(JVM)は状況に応じて、画像データ(この例では整数型のnativeImgで参照)を保持しているネイティブリソースを確実に再生するためにImage1finalize()メソッドを呼び出します。ここで注意してほしいのは、finalize()メソッドはJVMによって特殊な扱いを受けるものの、任意のコードからなる任意のメソッドだという点です。具体的には、finalize()メソッドはオブジェクト内のすべてのフィールド(この例ではposdim)にアクセスできます。また、驚くべきことに、finalize()メソッドの中でオブジェクトを再び到達可能にすることもできます(たとえばrandomImg = this;として静的フィールドから到達可能にするなど)。このプログラミング手法はあまりお勧めしませんが、方法としては可能です。

 ここで、ファイナライズ可能なオブジェクト(クラスに有効なファイナライザが定義されているオブジェクト)が作成されてから再生されるまでの流れを見てみましょう。オブジェクトの名前はobjとします(図1を参照)。

図1 ファイナライズ可能なオブジェクトobj
図1 ファイナライズ可能なオブジェクトobj
  1. objが割り当てられるときに、JVMはobjがファイナライズ可能であることを内部に記録します(通常はこれによってJVMの高速割り当てパスの速度が低下します)。
  2. ガベージコレクタがobjを到達不能と判断します。このとき、ガベージコレクタは割り当て時の記録からobjがファイナライズ可能であることを認識し、このオブジェクトをJVMのファイナライズキューに追加します。また、その時点で到達不能になっているオブジェクトでも、それがobjから到達可能なオブジェクトであれば、ファイナライザからアクセスされる可能性があるのですべて保持します。図2に、Image1インスタンスの場合の例を示します。
  3. その後、JVMのファイナライザスレッドobjをキューから取り出し、objfinalize()メソッドを呼び出して、ファイナライザが呼び出されたことを記録します。この時点で、objは「ファイナライズ済み」と見なされます。
  4. ガベージコレクタが再びobjを到達不能と判断しますが、今度はファイナライズ済みなので、objのスペースと、objオブジェクトから到達可能なすべてのオブジェクト(ただし他から到達不能なもの)を再生します。
図2 ガベージコレクタがobjを到達不能と判断する
図2 ガベージコレクタがobjを到達不能と判断する

 ガベージコレクタがobjを再生するのに最低でも2サイクルを必要とし、このプロセス中はobjから到達可能なすべてのオブジェクトを保持する必要があることに注意してください。この点を忘れていると、一時的に予想外のリソース保持問題が発生する可能性があります。もう1つ注意してほしいのは、JVMは、割り当てられたすべてのファイナライズ可能なオブジェクトのファイナライザを呼び出すとは限らないという点です。ガベージコレクタが一部のオブジェクトを到達不能と見なす前にJVMが終了することもあります。

サブクラス作成時のメモリ保持問題を回避する

 ファイナライズを明示的に使っていない場合でも、リソースの再生に遅延が生じる可能性があります。次に例を示します。

public class RGBImage1 extends Image1 {
    private byte rgbData[];
}

 RGBImage1Image1の継承クラスで、フィールドrgbData(およびこの例では示されていない一部のメソッド)を継承します。RGBImage1でファイナライザを明示的に定義しなかったとしても、このクラスはImage1からfinalize()メソッドを自然に継承するので、RGBImage1のインスタンスもすべてファイナライズ可能と見なされます。RGBImage1のインスタンスが到達不能になった場合、このインスタンスに含まれるrgbData配列は、インスタンスがファイナライズされるまで再生されません(図3を参照)。rgbData配列は非常に大きくなることもあるので、この配列の再生が遅れるとリソースの使用効率が低下します。ファイナライザが深いクラス階層の中に「隠されている」場合、この問題の原因を見つけるのはなかなか困難です。

図3 インスタンスがファイナライズされるまでrgbData配列は再生されない
図3 インスタンスがファイナライズされるまでrgbData配列は再生されない

 この問題を回避する1つの方法は、次のように、「継承」パターンではなく、「包含」パターンを使うようにコードを修正することです。

public class RGBImage2 {
    private Image1 img;
    private byte rgbData[];

    public void dispose() {
        img.dispose();
    }
}

 RGBImage1と異なり、RGBImage2Image1の継承クラスではなく、Image1のインスタンスを包含しています。RGBImage2のインスタンスが到達不能になると、ガベージコレクタは直ちにRGBImage2のインスタンスとrgbData配列(他のオブジェクトから到達不能になっている場合)を再生し、Image1インスタンスだけをファイナライズキューに追加します(図4を参照)。クラスRGBImage2Image1のサブクラスではないため、メソッドを継承しません。したがって、RGBImage2には、Image1の必須メソッド(たとえばdispose()メソッド)にアクセスする委譲メソッドを追加する必要があります。

図4 ガベージコレクタはImage1インスタンスだけをファイナライズキューに追加する
図4 ガベージコレクタはImage1インスタンスだけをファイナライズキューに追加する

 しかし、必ずしも前述のようにコードを修正できるとは限りません。修正できない場合は、クラスのユーザーとして、インスタンスがファイナライズ時に必要以上のリソースを消費しないようにもう少し工夫する必要があります。次のコードを参照してください。

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にするのはあまりお勧めできませんが、これは例外的なケースです。

図5 RGBImage3インスタンスの使用後にdispose()を呼び出す
図5 RGBImage3インスタンスの使用後にdispose()を呼び出す

メモリ保持問題からユーザーを解放する

 前節では、ファイナライザを使うサードパーティ製クラスを利用するときのメモリ保持問題の回避方法について説明しました。今度は、自分でクラスを開発する場合を想定し、ユーザーにこのような手間をかけさせないために、事後クリーンアップを要求するクラスの作成方法を考えてみましょう。一番よい方法は、クラスを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を参照)。クラスNativeImage2finalと宣言されているので、ユーザーはNativeImage2のサブクラスを作成できず、前述のメモリ保持問題が再現されることはありません。

図6 Image2インスタンスが到達不能になった場合はNativeImage2インスタンスだけがキューに追加される
図6 Image2インスタンスが到達不能になった場合はNativeImage2インスタンスだけがキューに追加される

 細かいことを言えば、NativeImage2Image2の内部クラスであってはなりません。内部クラスのインスタンスは、作成元である外部クラスのインスタンスの暗黙的参照を持ちます。したがって、NativeImage2Image2の内部クラスで、NativeImage2インスタンスがファイナライズキューに追加された場合は、対応するImage2インスタンスも保持されることになります。これは、まさに回避しようとしている問題です。しかし、NativeImage2クラスがImage2クラスからのみアクセス可能ならどうでしょうか。NativeImage2クラスにパブリックメソッドを用意していないのはこのためです(クラスだけでなく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(); }
}

 Image3Image2と同じです。NativeImage3は、事後クリーンアップがファイナライズではなく弱参照を利用している点以外はNativeImage2と同じです。NativeImage3WeakReferenceの継承クラスで、参照対象はImage3インスタンスです。参照オブジェクト(この場合はWeakReference)の参照対象が到達不能になると、参照オブジェクトは所定の参照キューに追加されます。今回の例ではnativeImgを参照オブジェクトに埋め込んでいるため、JVMは必要なものだけを参照キューに追加します(図7を参照)。繰り返しますが、前述の理由のとおり、NativeImage3Image3の内部クラスにしてはなりません。

図7 nativeImgを参照オブジェクトに埋め込む
図7 nativeImgを参照オブジェクトに埋め込む

 参照オブジェクトの参照対象がガベージコレクタによって再生処理されたかどうかを判断する手段としては、明示的な方法(参照オブジェクトの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におけるファイナライズの実装方法によく似ています。大量のネイティブリソースを明示的に使い、クリーンアップ時に細かい制御を必要とするプロジェクトでは、このアプローチをお勧めします。

 本稿では、ファイナライズの使用時に発生する2種類の問題、つまりメモリ保持問題とリソース保持問題だけを取り上げました。ファイナライズの使用とReferenceクラスが原因で、非常に微妙な同期問題が発生することもあります。詳しくは、『Finalization, Threads, and the Java Technology-Based Memory Model』(Hans-J. Boehm著)を参照してください。

ファイナライズは必要なときだけ使う

 本稿では、ファイナライズがJVMでどのように実装されているかを簡単に説明しました。ファイナライズ可能なオブジェクトによってメモリが不必要に保持される場合の例を示し、このような問題の解決策を紹介しました。最後に、ファイナライズの代わりに、柔軟性が高く予測可能な方法で事後クリーンアップを実行できる弱参照の利用方法を説明しました。

 ただし、不足する可能性のあるネイティブリソースを再生するという目的のために、ガベージコレクタに全面的に頼って到達不能なオブジェクトを特定するというやり方には重大な欠陥があります。大量のメモリを使ってわずかばかりのリソースを保護しても、あまり意味はありません。そのため、ネイティブリソースが関連付けられているオブジェクト(GUIコンポーネント、ファイル、ソケットなど)を使う場合は、終了時に必ずdispose()または同等のメソッドを呼び出すようにします。これで、ネイティブリソースの再生が速やかに行われ、リソース不足の可能性が軽減されます。本稿で紹介した事後クリーンアップのアプローチは、中心的なクリーンアップ手段ではなく、最後の手段として使用してください。

 また、ファイナライズの使用は、絶対に必要な場合だけにしてください。ファイナライズは、不確定的であるだけでなく、場合によっては予測不可能なプロセスです。利用する回数が少なければ少ないほど、JVMとアプリケーションに与える影響も小さくなります。

謝辞

 本稿に対して数々の建設的意見を寄せてくれたPeter Kesslerに感謝します。

商標

 Javaは、Sun Microsystems, Inc.の米国およびその他の国における商標または登録商標です。

著者紹介

Tony Printezis(Tony Printezis)
SunLabsでの3年以上の勤務を経て、Sun MicrosystemsのJava HotSpot Virtual Machine開発チームに参加。主に、ガベージコレクタのスケーラビリティ、反応性、並列性、ビジュアル化を中心に、動的メモリ管理に携わる。
関連テーマ
最新トップニュース
データメーション
【データメーション】
次の「Beverly Hills Chihuahua」はDVD違法コピー対策犬(10月15日)
ベンチャー専門家の目利きブログ「なぜこの企業は伸びるのか?」
【ベンチャー専門家の目利きブログ「なぜこの企業は伸びるのか?」】
「情報が気持ちよく伝わる世界を作る!」/アスカティースリー株式会社(10月15日)
Graphic Design Forum
【Graphic Design Forum】
あなたならどうする - 倫理にかかわる問題 (10月14日)
エンジニアの独り言
【エンジニアの独り言】
得体の知れない情報(?)との向き合い方(9月17日)
最新テクノロジーの意外な処方箋
【最新テクノロジーの意外な処方箋】
昆虫と退屈なことについて(9月16日)
「IT の耳」
「IT の耳」
【書評】『メディアの実験集「モノサシに目印」』 ――デザインを遊ぶ(10月15日)
Eメールマーケティング事情
Eメールマーケティング事情
企業メルマガ担当者(10月15日)
日本と韓国のインターネットビジネス最新動向調査
日本と韓国のインターネットビジネス最新動向調査
日本と韓国の Blog 比較2―展望(10月15日)
百式のネットビジネス研究
百式のネットビジネス研究
iPhone 用アプリを毎日作ってソースコードごと公開している「Apps Amuck」(10月15日)
SNSをビジネスに活用しよう
SNSをビジネスに活用しよう
周到に Hype(ハイプ)を迎えないと「幻滅期」は超えられない。(後篇)(10月14日)
developer.com
developer.com
デザインパターンの使い方: Builder(10月14日)
エンジニア転職ノウハウ開発室
エンジニア転職ノウハウ開発室
職種別 採用天気予報 [08年10〜12月期](10月14日)
アイレップの SEM フロンティア
アイレップの SEM フロンティア
キーワードとプレースメントの併用で Google AdWords 広告の最適化を進めよう(10月14日)
台湾企業が席巻する電子製品製造
台湾企業が席巻する電子製品製造
蔓延する市場の不透明感、不況の今だからこそ考える生産アウトソーシング(10月10日)
IT マネジメント
IT マネジメント
「後戻りできない」 Windows 7(10月10日)
海外のインターネットコムアメリカ韓国ドイツトルコ
Copyright 2008 Jupitermedia Corporation All Rights Reserved.http://www.internet.com/