NetBeans プロファイラのケーススタディー


1. はじめに

NetBeans プロファイラは、Sun が開発した Jfluid と呼ばれるプロファイリングテクノロジに基づいています。筆者は Jfluid が気に入っています。いくつかの Java アプリケーションでメモリーリークやパフォーマンスボトルネックを検出する作業がこのお陰で楽になりました。Jfluid テクノロジは NetBeans に統合されつつあります。現在、NetBeans 4.0 でこれを試すことができます。この記事では Profiler Milestone 3 ビルドを使用します。筆者は、これで開発者がアプリケーションを改善する作業がいかに楽になるかについての経験を、共有したいと思っています。できるのは、皆さんに入口を示すことだけです。皆さんがそこを通る必要があります。

NetBeans プロファイラをはじめて使用するという方は、まず次の URL にアクセスしてください。プロファイラを設定するためのドキュメントがいくつかあります。


2. メモリーリーク問題を検出する方法

プロファイラでメモリーリーク問題を検出するための原則を、次に示します。メモリーリークを検出するには、次の手順に従います。

  • JVM のヒープサイズとその傾向を監視します。
  • 使用済みヒープのサイズが増え続けている場合は、JVM ヒープの大部分を占有しているオブジェクトをチェックします。
  • そのオブジェクトの割り当てや解放が、どこでどのように行われているかを確認します。
  • インラインコード内でそのオブジェクトの割り当てや解放が適切に行われているかチェックします。

プロファイラでどのようにすれば上記の手順を実行できるのでしょうか。プロファイラのいくつかのグラフに目を通すことで、それを行えます。

「プロファイラ」ウィンドウのヒープグラフ

まず、NetBeans ウィンドウの最下部にある「プロファイラ」ウィンドウを見ましょう。左の図は、ヒープサイズと使用済みヒープのグラフです。これで、ヒープサイズと使用済みヒープがアプリケーションの実行中にどのように変化したかがわかります。使用済みヒープが時間とともに増えている場合は、メモリーリーク問題が発生している可能性があります。

メモリーリークの検出

上の図は、JVM がメモリーをどのように使用するかを簡単に説明しています。この場合、左のグラフの使用済みヒープは比較的安定しています。時間とともに増え続けていないからです。

ライブオブジェクトメモリーグラフ

次に、メモリーグラフに目を通し、メモリーの大部分を占有しているオブジェクトを確認しましょう。グラフの「ライブバイト数」フィールドには、各オブジェクトが使用しているメモリーサイズが表示されます。「ライブオブジェクト数」フィールドは、JVM ヒープ内のオブジェクトの数を表します。これらによって、どのオブジェクトがメモリーを占有しているかがわかります。「世代数」フィールドも見てください。「世代数」は、生き残った世代の数を表します。あるクラスのオブジェクトが時間とともに継続的に割り当てられ、解放されなかった場合、「世代」の数が増え続けます。これが典型的なメモリーリークの状況であり、このフィールドはその指標となります。

上記の「ライブオブジェクトメモリーグラフ」の例では、HashMap$Entry が、メモリーの多くの領域を占有しています。ただし、「世代数」フィールドの数値は低くなっています。メモリーリークが存在するかどうかの結論を出すことは困難です。アプリケーションをより長い時間実行し、そのメモリーの使用状況を監視する必要があります。

逆呼び出しグラフ

疑わしいオブジェクトが存在する場合は、逆呼び出しグラフを見ましょう。これにより、メモリーを占有しているオブジェクトがどこで割り当てられているのかを判断しやすくなります。これにより、Java コードのどこを見るべきか見当がつきます。

上の図は、「char」オブジェクトの逆呼び出しグラフを示したものです。このグラフから、「char」が StringBuffer.expandCapacity(int) メソッド内で割り当てられていることがわかります。逆呼び出しグラフから、問題のオブジェクトが割り当てられている場所を特定できます。その後、インラインコードのその場所を見ます。


3. ケーススタディー: メモリーリークの検出

次に、プロファイラでのメモリーリーク問題の検出に関するケーススタディーを紹介します。NetBeans プロファイラは、オブジェクトライブプロファイリング (オブジェクト作成とガベージコレクションの両方を記録) の機能を備えています。これは、あらゆるメモリーの問題が検出しやすくなるように、オブジェクトの割り当てと割り当て解除に関する統計を示します。

サンプルコード

プロファイラでのメモリーリークの検出を示す目的で、次のサンプルコードを使用します。

[TestHash サンプルコード]


/*
 * Main.java
 *
 * Created on 2005/01/06, 11:00
 */

package memoryleak;

import java.util.*;
import java.io.*;

/**
 *
 * @author root
 */
public class Main {
    Hashtable hashtable = new Hashtable();
    Square sq[] = new Square[10];
   
    /** Main の新しいインスタンスを作成します */
    public Main() {        
        
    }
    
    /**
     * @param args コマンド行引数
     */
    public static void main(String[] args) {
        // TODO アプリケーションロジックをここにコーディング
        Main testhash = new Main();   
        for(int i=0; i<100000; i++) {
            testhash.create(i);  
            testhash.use(i);    
            testhash.release();
        }
    }
    
    // Square オブジェクトを作成します。
    void create(int i) {
        for(int j=0; j<10; j++) {
            long index = j+i*10;
            sq[j] = new Square(index);
            hashtable.put(sq[j].num, sq[j]);
        }
    }
    
    // Square オブジェクトを使用します。
    void use(int i) {
        for(int j=0; j<10; j++) {
            System.out.print(((Square)(hashtable.get(sq[j].num))).square + " ");
        }
        System.out.println();
    }
    
    void release() {
        for(int j=0; j<10; j++) {
            sq[j] = null;
        }
    }
    
}

class Square {
    String num;
    String square;
    public Square(long num) {
        this.num = new Long(num).toString();
        this.square = new Long(num*num).toString();
    }
}

すでにこのサンプルコードの問題に気づいたかもしれませんが、このコードは次のような動作を行うものと仮定します。

  • Square オブジェクトを作成します。
  • 作成された Square オブジェクトを Hashtable 内に格納します。
  • Square オブジェクトを使用後に解放します。

最後に Square オブジェクトを解放しているので、メモリーリーク問題は含まれていないように見えます。

問題: OutOfMemoryError

それでは、サンプルコードを実行しましょう。問題がなければ、1 から 999999 までの数の 2 乗が表示されるはずです。実行結果を次に示します。

[実行結果]


0 1 4 9 16 25 36 49 64 81 
	:
	省略
	:
179411544900 179412392041 179413239184 179414086329 179414933476 
179415780625 179416627776 179417474929 179418322084 179419169241 
Exception in thread "main" java.lang.OutOfMemoryError

残念ながら、アプリケーションは OutOfMemoryError 例外で終了しました。

プロファイラを使用する

サンプルアプリケーションはなぜ OutOfMemoryError で終了したのでしょうか。OutOfMemoryError ということは、アプリケーションが JVM のヒープを使い果たしたと推測できます。しかし、どのオブジェクトがどうやってこのエラーを起こすのでしょうか。

NetBeans プロファイラを使用すべき時がやってきました。これは、新しいオブジェクトの割り当てと割り当て解除のプロファイルを作成します。プロファイラを実行するには、エクスプローラウィンドウでアプリケーションの主クラスを選択し、「プロファイル」->「ファイルをプロファイル...」を選択します。

次に、「メモリーの使用状況を解析」を選択します。「オブジェクト作成とガベージコレクションの両方を記録」を選択します。

「プロファイラ」ウィンドウのヒープグラフ

アプリケーションはプロファイラとともに実行されています。プロファイラの統計を表示するには、「プロファイル」->「現在の結果を取得」を選択します。第 1 ステップとして、ウィンドウ全体の最下部にある「プロファイラ」ウィンドウを見ます。「プロファイラ」ウィンドウのヒープグラフから、使用済みヒープ (紫色) のサイズが増え続けているのがわかります。したがって、メモリーリーク問題が発生している可能性があります。

[現在の結果の画像]

NetBeans ウィンドウの全体

ライブオブジェクトメモリーグラフ

次に、前出の図のライブオブジェクトメモリーグラフを見てください。メモリーの大部分を占有しているオブジェクトは、4 つあります。それは char[]、String、Hashtable$Entry、および Square です。また、前出の図の「世代数」フィールドも見てください。このフィールドは基本的に、メモリーリークの兆候を表します。この数が着実に増えていれば、それはおそらくメモリーリークの存在を意味しています。4 つのオブジェクトの「世代数」フィールドには比較的高い数が表示されています。これらは 51 です。これに対し、ほかのものは 1 です。この数が比較的高いので、これらが正しく解放されず、それによってメモリーリークが発生した可能性があります。

逆呼び出しグラフ

次に、リストの最上部にある「char」オブジェクトの逆呼び出しグラフを見ましょう。それには、「char」オブジェクト項目をダブルクリックします。

[char の逆呼び出しグラフ]

NetBeans ウィンドウの全体

逆呼び出しグラフによると、「char」オブジェクトの一部は String.<init> で作成されましたが、それらは一度も解放されていないようです。Square.<init> で作成された「char」では、「ライブオブジェクト数」フィールドと「割り当てオブジェクト数」フィールドの数が同じになっているからです。次に、「String」オブジェクトの逆呼び出しグラフを見ましょう。

[String の逆呼び出しグラフ]

NetBeans ウィンドウの全体

「char」と似た状況になっています。一部の「String」オブジェクトは、Long.toString() で作成されたあと、解放されていません。Long.toString() で作成された「String」では、ライブオブジェクトと割り当てオブジェクトの数が同じになっています。これらのオブジェクトが一度も解放されていないのは、なぜでしょうか。手掛かりは、ライブオブジェクトのリストの中にあります。リスト内の「Hashtable.$Entry」オブジェクトも、比較的高い「世代数」フィールドを持っています。「Hashtable.$Entry」は、Hashtable.remove() が呼び出されるまで存続します。そして、「Hashtable.$Entry」には、Hashtable に格納されたオブジェクトが含まれます。したがって、Hashtable 内に格納されたオブジェクトが適切に解放されなかったと推測できます。

インラインコードを確認する

では、サンプルプログラムのソースコードに戻りましょう。

[ソースからの抜粋]


    void release() {
        for(int j=0; j<10; j++) {
            sq[j] = null;
        }
    }

この部分です。これで、根本原因まであと少しです。「Square」オブジェクトは正しく「null」に設定されています。ところが、Hashtable 内の「Square」オブジェクトを解放するのを忘れていたようです。Hashtable には作成したオブジェクトが含まれています。「Square」オブジェクトには「String」オブジェクトが含まれていることがわかります。「Square」オブジェクト内の「String」オブジェクトがヒープ内に残っているようです。この問題を解決するため、ソースコードの「release」メソッドを次のように変更します。

[変更後のソース]


    void release() {
        for(int j=0; j<10; j++) {
            hashtable.remove(sq[j].num);  /* 追加 */
            sq[j] = null;
        }
    }

修正されたサンプルプログラムを実行しましょう。

[結果]


0 1 4 9 16 25 36 49 64 81 
 :
省略
 :
999980000100 999982000081 999984000064 999986000049 999988000036 
999990000025 999992000016 999994000009 999996000004 999998000001 

サンプルプログラムがエラーなしで終了しました。NetBeans プロファイラのメモリーの使用状況を監視する機能を使うと、メモリーリーク問題の原因を突き止めることができます。


4. まとめ

メモリーリーク問題を検出するための、NetBeans プロファイラの方法論とケーススタディーを紹介しました。最新の NetBeans プロファイラを試してみてください。JVM のメモリー問題の分析が楽になります。NetBeans Profiler とともにあらんことを。


By use of this website, you agree to the NetBeans Policies and Terms of Use. © 2013, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo