ここでは、サンプルプログラムleak.javaを使用して、Qualyzerの操作手順と解析方法の例を示します。なお、ここで使用している画面や結果、数値データはWindows(R) XP上で、特に断りがない場合JDK 5.0(シリアルGC)を用いて採取したものです。したがって、コマンドオプションの一部や、画面および結果が、利用者のハードウェアおよびOSにより異なることがありますので、あらかじめご了承ください。
■サンプルプログラム“leak.class”の実行
サンプルプログラム“leak.class”はボタンを1つ持つフレームを作ります。ボタンをクリックすると新しいフレームが現れ、その新しいフレームのボタンをクリックすると、また新しいフレームが現れると同時に自分自身を産み出したフレームが消えるという操作を繰り返すプログラムです。“leak.class”を実行します。
---------------------------------------------------------------------- C:\temp>java leak ----------------------------------------------------------------------
しかし、ボタンをクリックし続けると、ある時点でコンソール上に“java.lang.OutOfMemoryError”が発生します。
例)“java.lang.OutOfMemoryError”発生時のコンソールの表示
JDK/JRE 5.0の場合:
---------------------------------------------------------------------- C:\temp>java leak Exception in thread “AWT-EventQueue-0” java.lang.OutOfMemoryError: Java heap space The memory was exhausted on Java heap space. Java heap size / max Java heap size = 64506480 / 67108864 (*1) Java perm size / max Java perm size = 5613592 / 67108864 (*1) ----------------------------------------------------------------------
JDK/JRE 1.4の場合:
---------------------------------------------------------------------- C:\temp>java leak java.lang.OutOfMemoryError ----------------------------------------------------------------------
(*1):“java.lang.OutOfMemoryError”発生時のヒープのサイズ情報です。発生の環境により異なります。
■メモリ/GCの分析
同じ操作の繰り返しでメモリ不足が発生しているこのプログラムはメモリリークの可能性があります。そのため、まずメモリ/GC情報を採取します。
JDK/JRE 5.0の場合:
---------------------------------------------------------------------- C:\temp>java -XX:+UseSerialGC -Xrunfts:heap=1 leak (*1) Exception in thread “AWT-EventQueue-0” java.lang.OutOfMemoryError: Java heap space The memory was exhausted on Java heap space. Java heap size / max Java heap size = 64506480 / 67108864 Java perm size / max Java perm size = 5613592 / 67108864 C:\temp>java -jar fts.jar data=qua.3736 heap (*2) ----------------------------------------------------------------------
JDK/JRE 1.4の場合:
---------------------------------------------------------------------- C:\temp>java -Xrunfts:heap=1 leak (*1) java.lang.OutOfMemoryError C:\temp>java -jar fts.jar data=qua.3736 heap (*2) ----------------------------------------------------------------------
ここでは、以下の情報収集オプションを指定しています。(*1)
heap=1 : メモリ/GC情報を採取
また、以下の情報表示オプションを指定しています。(*2)
heap : メモリ/GC情報を表示
data=qua.3736 : 上記で収集された情報の格納先ディレクトリ
グラフ全体を見ると、GC発生を示す黄色の縦の帯が見えますが、消費メモリ量を示す赤いグラフは減少する様子がなく増加の一方です。これはメモリリークが存在しているときに見られる傾向です。
そのため、どのオブジェクトインスタンスがGCによって回収されずに残っているかを調べるためにQualyzerのメモリ(ヒープ)ダンプ機能を使います。
■ヒープ情報の収集と表示(メモリリーク発生時)
Qualyzerのメモリ(ヒープ)ダンプ機能を使って、メモリリーク発生時のヒープ情報を収集し、表示します。
JDK/JRE 5.0の場合:
---------------------------------------------------------------------- C:\temp>java -XX:+UseSerialGC -Xrunholmes:t=30 leak (*1) Exception in thread “AWT-EventQueue-0” java.lang.OutOfMemoryError: Java heap space The memory was exhausted on Java heap space. Java heap size / max Java heap size = 64506480 / 67108864 Java perm size / max Java perm size = 5613592 / 67108864 dumping ...done C:\temp>java -jar holmes.jar -i qua.1816 -o d:\work\html -heap (*2) dump.hlm を読込んでいます ... ******************** 100% liv.hlm を読込んでいます ... ******************** 100% ser.hlm を読込んでいます ... ******************** 100% HTML ファイルを生成しています ... ******************** 100% C:\temp>java -jar holmes.jar -i qua.1816 -h d:\work\html -view (*3) dump.hlm を読込んでいます ... ******************** 100% ----------------------------------------------------------------------
JDK/JRE 1.4の場合:
---------------------------------------------------------------------- C:\temp>java -Xrunholmes:t=30 leak (*1) java.lang.OutOfMemoryError dumping ...done C:\temp>java -jar holmes.jar -i qua.1816 -o d:\work\html -heap (*2) dump.hlm を読込んでいます ... ******************** 100% liv.hlm を読込んでいます ... ******************** 100% ser.hlm を読込んでいます ... ******************** 100% HTML ファイルを生成しています ... ******************** 100% C:\temp>java -jar holmes.jar -i qua.1816 -h d:\work\html -view (*3) dump.hlm を読込んでいます ... ******************** 100% ----------------------------------------------------------------------
以下のヒープ情報収集オプションを指定して、プログラムを起動します。 (*1)
t=30 : call stack情報を精査する深さを30として情報を採取
OutOfMemoryが発生するまでボタンクリックを繰り返します。
ボタンラベルnext14のボタンをクリックしたとき、OutOfMemoryが発生しました。
以下のオプションを指定して、HTMLファイルを生成します。 (*2)
-heap : ヒープ情報をHTMLファイルに生成
-i qua.1816 : ヒープ情報格納先ディレクトリ
-o d:\work\html : HTMLファイル格納先ディレクトリ
-viewオプションと、以下のオプションを指定して、ヒープ情報を表示します。 (*3)
-i c:\temp\qua.1816 : ヒープ情報格納先ディレクトリ
-h d:\work\html : HTMLファイル格納先ディレクトリ
メモリリーク発生時のクラス一覧が表示されます。
第1項目はallocatedで作成されたオブジェクトの個数、第2項目はlivingで最終的に生き残ったオブジェクトの個数、そしてnameはクラス名または配列クラス名です。
■ヒープ情報の収集と表示(メモリリーク発生前)
上記と同様の手順で、メモリリーク発生前のヒープ情報を収集し、表示します。
ここでは、ボタンラベルnext14のボタンが表示段階でボタンクリックを終了し、HTMLファイルを生成します。(ここではHTMLファイルをディレクトリd:\work\htmlに生成しています。)
JDK/JRE 5.0の場合:
---------------------------------------------------------------------- C:\temp>java -XX:+UseSerialGC -Xrunholmes:t=30 leak dumping ...done C:\temp>java -jar holmes.jar -i qua.1936 -o d:\work\html -heap dump.hlm を読込んでいます ... ******************** 100% liv.hlm を読込んでいます ... ******************** 100% ser.hlm を読込んでいます ... ******************** 100% HTML ファイルを生成しています ... ******************** 100% C:\temp>java -jar holmes.jar -i qua.1936 -h d:\work\html -view dump.hlm を読込んでいます ... ******************** 100% ----------------------------------------------------------------------
JDK/JRE 1.4の場合:
---------------------------------------------------------------------- C:\temp>java -Xrunholmes:t=30 leak dumping ...done C:\temp>java -jar holmes.jar -i qua.1936 -o d:\work\html -heap dump.hlm を読込んでいます ... ******************** 100% liv.hlm を読込んでいます ... ******************** 100% ser.hlm を読込んでいます ... ******************** 100% HTML ファイルを生成しています ... ******************** 100% C:\temp>java -jar holmes.jar -i qua.1936 -h d:\work\html -view dump.hlm を読込んでいます ... ******************** 100% ----------------------------------------------------------------------
メモリリーク発生前のクラス一覧が表示されます。
■分析
ユーザのプログラムで定義しているleakクラスに注目してみます。クラス一覧(メモリリーク発生前)とクラス一覧(メモリリーク発生時)を比べると、leakクラスのどのオブジェクトの生き残り個数も増加していることがわかります。また、生成されたオブジェクト数と生き残りのオブジェクト数が同じであるため、GCによって1つも回収されていないことがわかります。特にleak$genframeは、以下のように内部でサイズの大きなバイト型配列を生成しています。
---------------------------------------------------------------------- void doSomething() { chunk1 = new byte[1000000]; chunk2 = new byte[1000000]; chunk3 = new byte[1000000]; chunk4 = new byte[1000000]; } ----------------------------------------------------------------------
次に、なぜこのleak$genframeクラスオブジェクトが回収されないのか調べます。
leak$genframeは、ソースを見るとleakクラスのコンストラクタとleak$genframe$1クラス(ActionListenerの名前なしインナークラス)で生成されています。生き残りオブジェクト数はleak$genframe$1の方が多いので、こちらを中心に調べます。leak$genframe$1の生き残りオブジェクト数部分をクリックすると生き残りオブジェクト一覧が表示されます。
GCで回収されないのはどこからかの参照が残っているためです。それを調べるためにreachable chain部分をクリックします。
表示される内容はすべて同じではありませんが、すべてに共通するオブジェクトとして“javax.swing.event.EventListenerList”があります。このクラスはイベントリスナーに登録されたオブジェクトリストを保存しているクラスです。そのため、ボタンのActionListenerに自分自身が登録されており、ボタンからの参照がまだ切れていないため解放されていないと考えられます。
■プログラムの改良例
“improved.java”は“leak.java”に対し、ボタンからの参照を切るため、ボタンのActionListenerに次の修正を加えています。
修正前 ---------------------------------------------------------------------- button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { new genframe(self); } }); ----------------------------------------------------------------------
修正後 ---------------------------------------------------------------------- button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { new genframe(self); // These prevent memory leak. button.removeActionListener(parentlistener); parentlistener = null; button.removeActionListener(this); } }); ----------------------------------------------------------------------
改良されたプログラムimprovedでメモリ/GC情報を採取してみます。
JDK/JRE 5.0の場合:
---------------------------------------------------------------------- C:\temp>java -XX:+UseSerialGC -Xrunfts:heap=1 improved C:\temp>java -jar fts.jar data=qua.1472 heap ----------------------------------------------------------------------
JDK/JRE 1.4の場合:
---------------------------------------------------------------------- C:\temp>java -Xrunfts:heap=1 improved C:\temp>java -jar fts.jar data=qua.1472 heap ----------------------------------------------------------------------
消費メモリのグラフが、GCの発生に応じて減っていることが確認できます。
■サンプルソース
------------------------------------------------------------------------------- /* All Rights Reserved. Copyright Fujitsu Limited, 2002. */ import java.awt.*; import java.awt.event.*; import javax.swing.*; public class leak { static int generation = 0; class genframe extends JFrame implements ActionListener { byte[] chunk1, chunk2, chunk3, chunk4; // Nothing useful. Waste memory. genframe self; // reference of myself final JButton button; genframe(ActionListener parent) { super("generation: " + leak.generation); leak.generation ++; self = this; addWindowListener(new winListener()); button = new JButton("next" + leak.generation); // Add action listner to the button. // This listner will create a new frame. button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { new genframe(self); } }); // This listner will dismiss the parent frame. if(null != parent) button.addActionListener(parent); getContentPane().add(button); setSize(200, 100); setVisible(true); doSomething(); } public void actionPerformed(ActionEvent ae) { dispose(); } // This method does nothing usefull. // If these chunks won't be garbage collected, // OutOfMemory Exception will be occurred soon. void doSomething() { chunk1 = new byte[1000000]; chunk2 = new byte[1000000]; chunk3 = new byte[1000000]; chunk4 = new byte[1000000]; } } leak() { new genframe(null); } class winListener extends WindowAdapter { public void windowClosing(WindowEvent e) { System.exit(0); } } public static void main(String[] arg) { new leak(); } } -------------------------------------------------------------------------------
------------------------------------------------------------------------------- /* All Rights Reserved. Copyright Fujitsu Limited, 2002. */ import java.awt.*; import java.awt.event.*; import javax.swing.*; public class improved { static int generation = 0; class genframe extends JFrame implements ActionListener { byte[] chunk1, chunk2, chunk3, chunk4; // Nothing useful. Waste memory. genframe self; // reference of myself final JButton button; ActionListener parentlistener; genframe(ActionListener parent) { super("generation: " + improved.generation); improved.generation ++; self = this; addWindowListener(new winListener()); parentlistener = parent; button = new JButton("next" + improved.generation); // Add action listner to the button. // This listner will create a new frame. button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { new genframe(self); // These prevent memory leak. button.removeActionListener(parentlistener); parentlistener = null; button.removeActionListener(this); } }); // This listner will dismiss the parent frame. if(null != parent) button.addActionListener(parent); getContentPane().add(button); setSize(200, 100); setVisible(true); doSomething(); } public void actionPerformed(ActionEvent ae) { dispose(); } // This method does nothing usefull. // If these chunks won't be garbage collected, // OutOfMemory Exception will be occurred soon. void doSomething() { chunk1 = new byte[1000000]; chunk2 = new byte[1000000]; chunk3 = new byte[1000000]; chunk4 = new byte[1000000]; } } improved() { new genframe(null); } class winListener extends WindowAdapter { public void windowClosing(WindowEvent e) { System.exit(0); } } public static void main(String[] arg) { new improved(); } } -------------------------------------------------------------------------------