短命の Java アプリケーションならまだしも、サーバーのようなデーモンタイプの長期間稼働するような Java アプリケーションのメモリリークはやっかいですよね。放っておくと OutOfMemoryError でダウンしてしまいますからね。
定期的に再起動という運用対処も場合によってはありですが、アプリケーションのバグが原因だとしたらやはりそれは問題を特定して改修したいところです。
でも、特に何もしているわけでもないのに少しずつ、そして確実にメモリリークしていくようなびみょ〜な場合は問題特定のためのとっかかりもなくてやっかいです。アプリケーションの中身を知っているのならまだしも、よくわからない 3rd パーティー製のアプリケーションだったり、巨大化・複雑化したアプリケーションだとなおさらやっかいです。
メモリリークを特定するような専用ツールとかあればベストですが、商用製品で値段がお高かったり、計測中はパフォーマンスに影響を与えるので長いこと監視するのに不向きだったり…。
Java アプリケーションのメモリリーク問題を、Java ヒープ領域に限定するのであれば、JVM の機能を使って、Java オブジェクトのヒストグラム(現在の Java ヒープ内のオブジェクトのクラス名とその数、総サイズ一覧を表示、とか)を取得することがとっかかりとしてはお手軽ですね。
で、そのヒストグラムを、メモリリークの影響がでる前にまず 1 回取得して、メモリリークの影響が顕著になってきたら再度取得して、両者を比較すると、おおよそ増えているオブジェクトのクラス名がわかるので、どこでリークしているか調査の範囲を絞り込むことができます。
ということで、まずは一般的に広く使われている Oracle JVM で Java オブジェクトのヒストグラムを取得してみましょう。JRE だけでなくて、JDK がインストールされていることを前提に、jps コマンドで取得対象となる Java アプリケーションのプロセス ID を確認します。
そして、以下のように jmap コマンドでヒストグラムを取得できます。
jmap -histo:live [対象 Java アプリケーションのプロセス ID]
# :live をつけてあると、GC 対象になってないライブオブジェクトのみが対象となります。
これだけだと、標準出力にだらだらと表示されるだけなので、ファイルにリダイレクトして保存しておきます。ちなみに、Oracle JVM(java 6 で確認)の場合は以下のような感じでヒストグラムが出力されます。
num #instances #bytes class name
- -
1: 23292 3203176
2: 51094 2720568
3: 23292 1864448
4: 1834 1431656
5: 10183 1224736 [C
6: 1431 895768
7: 8193 894736 [B
:
11: 10568 253632 java.lang.String
12: 7552 241664 java.util.concurrent.ConcurrentHashMap$Segment
13: 2009 192864 java.lang.Class
:
左から、通し番号、インスタンス(オブジェクト)数、総サイズ、クラス名、です。総サイズの大きい順にソートされております。
さてこれからが本題なのですが、メモリリークビフォー・アフターで取得した 2 つのヒストグラムをどう比較するかです。手でやるとか、Excel に貼り付けてマクロでとか考えられますが、クラスの数が多いと大変ですよね。なので、だれかが作っているかもしれませんが、ヒストグラムから Java ヒープ領域のメモリリーク発見支援ツールを java で作りましたのでさらしてみます、というのがこのブログエントリの趣旨です。
ツール作成時のポイントとしては、いままでの経験からすると 1 つだけです。本ブログエントリのタイトルにもある通りなんですが、以下です。
- オブジェクト数で差分を比較して、差分の大きい順にソートして表示
冒頭に言及したように、びみょ〜なメモリリークは、リークしているオブジェクトのサイズ自体が小さいけどオブジェクト数だけは確実に増えているということがあり、その場合はオブジェクト数で差分をとってソートした方が一目瞭然なので(個人の見解です)。
ということで、以下ツールを添付します。jar でかためてあるだけです。ソースコードも入ってますが見る価値はないです。
JavaObjDiff-v0.1.jar
とりあえず、Oracle JVM というか、上述の jmap で取得するテキストフォーマットのヒストグラムしか想定してません。
使い方ですが、添付 jar ファイルを CLASSPATH に通した状態で以下のコマンドを実行します。
java quitada.JavaObjectDiff [メモリリーク前のヒストグラムファイル] [メモリリーク後のヒストグラムファイル]
出力結果は以下のような感じです。
diff class name
- -
10828 [B
10244 java.util.concurrent.ConcurrentHashMap$HashEntry
9872 java.lang.Integer
5789 java.util.concurrent.locks.ReentrantLock$NonfairSync
5632 java.util.concurrent.ConcurrentHashMap$Segment
5632 [Ljava.util.concurrent.ConcurrentHashMap$HashEntry;
2429
:
-2 sun.reflect.NativeConstructorAccessorImpl
-2 java.net.InetAddress$CacheEntry
-8 java.util.ResourceBundle$CacheKey
-8 java.util.ResourceBundle$BundleReference
-8 java.util.ResourceBundle$LoaderReference
左から、オブジェクト数の増加量、クラス名、です。オブジェクト数の増加量が多い順にソートされております。オブジェクト数が減ってしまったものは、マイナス表示になってます。ま、上の方に列挙されているクラス由来のオブジェクトが、よりリークしている可能性が高いということになります。