オルタナティブ・ブログ > The Grouchy Bug >

米国東海岸発、とあるソフトウェア開発者のよもやま話

クラッシュは忘れた頃にやってくる: Javaのメモリーリーク

»

今回はJavaのアプリケーションにおける、メモリーリーク、リソースリークのお話です。

メモリーリークは突然クラッシュを起こす原因のひとつ

PCの電源を切らずに、あるアプリケーションを終了せずに何日も使っていると、ある日突然クラッシュしたということを体験したことがあるかもしれません。原因はいろいろありますが、その中でよくあるのが、メモリーリークやリソースリークと呼ばれるものです。メモリーリークというのはそのアプリケーションが使っているうちに、その使用メモリ量がどんどん大きくなることです。またリソースリークとは、そのアプリケーションが使っている、スレッドの数、イメージの数、ファイルの数などがどんどん大きくなることです。アプリケーションが使えるメモリやリソース量は上限があります。この上限を越えると、最悪、突然終了、つまりクラッシュしてしまいます。リークは、ユーザーがあるアクションを行ったときに起きて、そのアクションを何度もくりかえしたりすることで、最終的にはクラッシュが起こったりします。

なんでJavaなのにメモリリークが起きるのか


Javaは、ガベージコレクタが組み込まれています。CやC++の場合と違って、アプリ開発者のコードはメモリを解放する必要はありません。使われないオブジェクトはガベージコレクトされて自動的に削除されます。なので、メモリーリークは起きません。しかし、それはそのオブジェクトが誰にも使われていない(つまりだれにも参照されなくなった)場合の話で、誰かがそのオブジェクトを参照している限り、そのオブジェクトはメモリ上に残って、削除されません。それがJavaのメモリーリークです。


大規模なソフトウェア製品におけるクラッシュ対策

大規模なソフトウェア製品は、多くの開発チームによって開発されます。それは、ある開発者が開発したコンポーネントが複数の他の開発者が開発したコンポーネントから使われる(参照される)可能性があるということです。通常、テストチームは、何十時間もしくは何日にもおよぶテストを通して、long runによるクラッシュや異常動作がないかをチェックします。その後、問題のあった箇所に対して、デバッガやプロファイラなどを使って、調査、デバッグを行っていきます。その際リークが原因であれば調査されます。または、パフォーマンステストを通して、ある主要なユーザーアクセス/アクションに対してリークがないかどうかもチェックされるというのが一般的だと思います。そこで、重要になってくるのが、リークの起こりにくいコードを書くということです。

よくあるメモリーリークのバグパターン

よくあるメモリーリークの例を上げてみます。

1. コレクションオブジェクトメンバーへputするコードのみ存在し、removeするコードがない場合
class A {
    private HashMap map = new HashMap(10);
   
    public void methodA(Object keyObj, Object valueObj) {
        map.put(keyObj, valueObj);
    }
}

これはmap.remove()がこのクラスにないのが問題です。もしこのAオブジェクトがそのアプリケーションの起動から終了まで使われ続けた場合、Aオブジェクトのmapメンバーはputするたびに大きくなっていき、ひどい場合は、顕著なメモリーリークとなって、クラッシュをおこすでしょう。

2. コレクションオブジェクトのメンバーに、put(obj, null)をコールしている場合
class B {
    private HashMap map = new HashMap(10);
   
    public void methodA(Object keyObj, Object valueObj) {
        map.put(keyObj, valueObj);
    }

    public void methodB(Object keyObj) {
        map.put(keyObj, null);
    }
}
put(keyObj, null)は、keyがkeyObjで、valueがnullのエントリーをそのHashMapの中に作成するということです。そのエントリーを削除するということではありません。特にkeyObjのあるメンバーが他のオブジェクトを参照していた場合、それがリークの原因となることがあります。エントリーを削除したいなら、かならずremove()を呼ぶことが重要です。


3. リスナーを追加するコードのみで、削除するコードが存在しない

class C {
    private static IWorkbench workbench = PlatformUI.getWorkbench();


    public C(Window window) {
        this.window = window;
        workbench.addWindowListener(new IWindowListener() {
            // ...
        });
    }
}
addXXXXListener()したら、removeXXXXListener()しましょうということです。さらに、上の例では、removeWindowListenerがないために、new IWindowListenerがリークし、その結果、Cオブジェクト自身(Cオブジェクトとそのメンバー全部)もリークしてしまっています。

4. 自分がいらなくなったのにも関わらず、自分のメンバーを削除していない
class D extends Composite {
    private HashMap map = new HashMap(100);
    private ArrayList listeners = new ArrayList(100);
    private Object memberA;

    public C(Composite parent, int style) {
        super(parent, style);
    }

    public void dispose() {
        super.dispose():
    }

    // more methods to access to map, listeners, memberA so on.
    // ...
}

Eclipse SWTのコントロールは常に、そのコントロールが削除されるときは、dispose()が呼ばれます。なので、dispose()がそのオブジェクトをクリーンアップするチャンスとなります。上の例では、Dのdispose()でメンバーすべてをクリーンアップしていません。もしDオブジェクトを参照している(使っている)他のオブジェクトにバグがあって、Dオブジェクトが他のオブジェクトによってリークさせられたとしたら、Dオブジェクトのメンバーもリークしてしまいます。なので、仕様済みのメンバーオブジェクトは、常に参照からはずしてしまうのがいいです。上の例だと、

public void dispose() {
    super.dispose();
    map = null;
    listeners = null;
    memberA = null;
}

という風にコードを書いておくのがベストプラクティスと言えるでしょう。

Eclipseではリソースリークもクラッシュの原因

Lotus NotesなどのEclipseベースの製品の場合、あるオブジェクトを使い終わった後は、必ずdispose()を呼ぶ必要があります。特にSWTのdispose()では通常OSの各種リソース、例えば、Handle, Thread, User Objectsなどを開放します。なので、dispose()が呼ばれないと、それらのOSリソースがリークします。


JNIでのC/C++コードでもリークする可能性がある

ここでは述べませんが、Eclipseベースの製品は、JNI(JavaからOSのAPIをコールするAPI)のコードを実装するというのもしばしばあります。JNIのコードは基本的にC/C++で、OS APIやOSネィティブコードにアクセスするコードですから、そこでもリークする可能性があるので、ここでのコードもリークフリーなコードにすることが重要です。

リークを特定するのに便利なツール

Javaのメモリーリークをテスト、デバッグするには、Javaのプロファイラーが便利です。IBM/Lotusのチームでは、JProfiler(http://www.ej-technologies.com/products/jprofiler/overview.html)やEclipse Memory Analyzer(http://www.eclipse.org/mat/)などを使ってメモリーリークのテスト、デバッグを行っています。

また、Javaのリソースリークについては、Eclipseが提供しているsleak(http://www.eclipse.org/swt/tools.php)を使います。また、Windowsだと、メモリーリーク、リソースリークを調べるのに、Task ManagerやProcess Explorer(http://technet.microsoft.com/en-us/sysinternals/bb896653.aspx)などが使われています。

ということで

何時間もたった後の突然のクラッシュの一因として、メモリーリーク、リソースリークがありますという話でした。ソフトウェア製品開発プロセスの中で、新機能の設計、実装、デバッグ、テスト、ときて、最後の方にでてくるのが、これらメモリーリークやリソースリークの問題だと思います。製品リリース前、開発プロセスの最後の方ででてくるので、けっこう忙しい中、開発者をなやませる問題だと思いますが、これらリーク問題、しっかりと取り組んで行きたいと思います。

Comment(0)