【JTS】Vol.004:プログラムのメモリリークを判定する方法

問題

プログラムを動かすと、徐々にメモリ使用量が増加していきます。プログラムが正しい動作をしているのか、それともメモリリークしているのかどうか判断がつきません。

Java OS
Java SE 5.0以降 any

解説

1週間・1ヶ月と長時間実行するプログラムで発生した場合に、最も問題になるのは、メモリリークです。メモリリークが発生するプログラムを長時間実行させると、メモリを使い尽くしてしまいます。メモリを使い尽くしたプログラムは、次のようにOutOfMemoryErrorを発生して、強制終了してしまいます。

Exception java.lang.OutOfMemoryError: requested 124 bytes for AAA

実運用と同じような長時間実行する試験は頻繁に出来ないため、プログラムを短い時間動かすだけで、メモリリークかどうかを分析する必要があります。

対策

Javaプログラムのメモリリークは、2つの領域で発生する場合があります。

  • Javaヒープ
  • Cヒープ

今回は、問題がおきやすいJavaヒープでのメモリリークについて説明します。

最初に、Javaヒープにおけるメモリリーク発見のための分析方法をまとめると、次のようになります。

  1. メモリ使用量全体が増えているか?増えて続けているか?
  2. どのオブジェクトがメモリリークしているのか?

ここでは、最初に1.のメモリ使用量の増加ついての確認を行い、次に2.のメモリリークしているオブジェクトを特定する方法を説明します。

1. メモリ使用量全体が増えているか?増えて続けているか?

メモリ使用量の増加を確認するためには、

  • (a) jstatを利用する方法と
  • (b) Javaの起動オプションにGCDetailsを指定する方法、の2つがあります。
(a) jstatを利用する

jstatを利用することで、Javaプロセスのメモリ利用状況を調べることができます。jstatは次のように利用します。

$ jstat -gc  

には、分析したいJavaプロセスのプロセスIDを指定します。JavaプロセスのプロセスIDは、jpsコマンドで調べることが可能です。下の実行結果では「3535」がプロセスIDになります。

$ jps
3535 MemoryLeakTest
3649 Jps

にはデータの更新周期をミリ秒単位で指定します。通常は、1000ミリ秒を指定すれば十分です。

jstatの結果は次のようになります。

$ jstat -gc 3770 1000
 S0C   S1C  S0U  S1U    EC     EU      OC    OU    PC     PU    
 64.0  64.0 0.0  16.0 5952.0 2144.2 28608.0 162.4 16384.0 1775.3
 64.0  64.0 0.0  16.0 5952.0 5476.5 28608.0 162.4 16384.0 1775.3
 64.0  64.0 16.0 0.0  5952.0 1547.7 28608.0 162.4 16384.0 1775.3
128.0 128.0 64.0 0.0  5952.0 4644.8 28608.0 162.4 16384.0 1775.3
128.0 128.0 0.0  16.0 5952.0 476.2  28608.0 162.4 16384.0 1775.3
128.0 128.0 0.0  16.0 5952.0 2976.4 28608.0 162.4 16384.0 1775.3
128.0 128.0 16.0 0.0  5952.0  0.0   28608.0 162.4 16384.0 1775.3
128.0 128.0 48.0 0.0  5952.0 3095.8 28608.0 162.4 16384.0 1775.3
※実際にはPUの項目以降も表示されますが、紙面の都合で省略しています。

大量の情報が表示されますが、注目するのはOUの項目です。この値が徐々に大きくなる場合には、メモリリークの発生の疑いがあります。

(b) Javaの起動オプションにGCDetailsを指定する

jstatを利用できない環境でも、JavaVMのGCログからメモリリークを判定できます。Javaの起動オプションに"-XX:+PrintGCDetails"を追加することで、GC発生のタイミングに、メモリの使用量がコンソールに表示されます。

2. PrintClassHistogramでリークしているオブジェクトを特定する

jstatやGCログの結果からメモリリークの疑いがある場合には、より詳細な分析を行い、リークしているオブジェクトを特定する必要があります。Javaの起動オプションに"-XX:+PrintClassHistogram"を追加して、再度プログラムを実行してください。起動したJavaプロセスにSIGQUITシグナルを送信することで、Full GCが強制実行され、Javaのヒープ使用状況がコンソールに表示されます。

シグナルは、Unix/Linuxであればkillコマンドを使って送信します。

$ kill -3 

Windowsでは、Javaプロセスを立ち上げたコマンドプロンプトで、Ctrl + Break を押してください。

シグナルを受信したJavaプロセスは、ヒープ内のオブジェクトごとに、インスタンス数と使用しているメモリ領域の合計を表示します。具体例は次のようになります。

num   #instances    #bytes  class name
                                                                          • -
1: 14903 2145496 2: 14903 1556904 3: 1304 1437232 4: 27346 1262560 5: 3118 1055688 [I 6: 12025 904296 [C 7: 1302 899448 8: 1133 881280 9: 2043 630176 [Ljava.lang.String; 10: 1993 513720 [B 11: 11503 460120 java.lang.String ...(省略)... Total 131338 14843688

メモリリークの発生を確認するには、時間を置いて何回かシグナルを送り、その結果を比較します。Totalのインスタンス数やバイト数が増加している場合には、メモリリークの可能性があります。また、インスタンス数やバイト数が上昇しているオブジェクトが、メモリリークの原因となっている可能性が高くなります。

まとめ

このようにjstatのOU, GCDetailsのヒープ使用量、PrintClassHistogramの結果を使ってメモリリークが発生している可能性を確認できます。最終的にメモリリークの発生を確認するには、実際にソースコードの分析をする必要があります。

追加情報

このように、従来のメモリリークの調査では、複数のコマンドを使いこなす必要がありました。また、メモリリークの発生を確認するためには、ソースコードの分析が必要になってしまいます。

しかし、当社のENdoSnipeを利用すれば、メモリリークが発生していることをグラフィカルに表示するため、容易に発生を確認することができます。更に、メモリリークの原因についても、該当箇所がソースコードのどこかを行番号レベルで特定することが可能です。

詳しい情報については、EndoSnipeの利用事例ページをご覧ください。