SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

japan.internet.com翻訳記事

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

JVMのファイナライズ処理の仕組みとテクニック

ファイナライズの代替手段

 前節の例では、まだ不確定要素が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.の米国およびその他の国における商標または登録商標です。

修正履歴

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
japan.internet.com翻訳記事連載記事一覧

もっと読む

この記事の著者

japan.internet.com(ジャパンインターネットコム)

japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.comEarthWeb.com からの最新記事を日本語に翻訳して掲載するとともに、日本独自のネットビジネス関連記事やレポートを配信。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

Tony Printezis(Tony Printezis)

SunLabsでの3年以上の勤務を経て、Sun MicrosystemsのJava HotSpot Virtual Machine開発チームに参加。主に、ガベージコレクタのスケーラビリティ、反応性、並列性、ビジュアル化を中心に、動的メモリ管理に携わる。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/337 2006/04/11 19:12

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー