Javaアプリでトラブルがあったりすると、アプリケーションのログなどを読んで解析をすると思いますが、パフォーマンスやGC周りでのトラブルについては、JDKに標準付属しているコマンドを使用すると原因究明の手がかりがつかめたりすることがあります。
自分も、ちょっと前にやっていたプロジェクトでは、かなり使うことになりました。どれも有名だとは思いますが、使い方をメモしておくという意味を込めて書いておきます。
まず、今回のサンプルとして、以下のような明らかに問題のあるプログラムを用意。
Monitor.java
import java.util.ArrayList; import java.util.List; public class Monitor { public static void main(String[] args) { int busyThreadNum = 3; int spareThreadNum = 10; for (int i = 0; i < busyThreadNum; i++) { new BusyThread(i).start(); } for (int i = 0; i < spareThreadNum; i++) { new SpareThread(i).start(); } } } class BusyThread extends Thread { public BusyThread(int num) { super("Busy-Thread-" + num); } @Override public void run() { int counter = 0; int limit = 100000; long sleepTime = 10L; List<String> list = new ArrayList<>(); while (true) { counter++; list.add("counter-" + counter); if (counter > limit) { try { Thread.sleep(sleepTime); } catch (InterruptedException e) { } counter = 0; list = new ArrayList<>(); } } } } class SpareThread extends Thread { public SpareThread(int num) { super("Spare-Thread-" + num); } @Override public void run() { long sleepTime = 1000L; while (true) { try { Thread.sleep(sleepTime); } catch (InterruptedException e) { } } } }
せっかくJava SE 7を使っているので、ダイアモンド演算子を使ってみました。
それでは、これを起動しておきます。
$ java Monitor
jps
コマンドを実行したマシンで起動している、JavaアプリケーションのPIDを取得することができます。まあ、psコマンドとかで代用しても、全く問題ないと思います。
$ jps 3835 Monitor 3861 Jps
jpsコマンド自身のPIDも出てくるので、これは除外しておきましょう。
ここで取得したPIDを、以降のコマンドで使用します。
jstat [option] [PID] [interval] [count]
JavaVMのヒープやGCの統計情報を取得するコマンドです。New(Suvivor/Eden)やOld、Permentの使用状況やGCの実行回数、実行時間などを確認することができます。
実行例。
$ jstat -gc 3835 1000 S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 16768.0 7424.0 0.0 7400.3 302144.0 216304.5 70912.0 35858.5 21248.0 2361.2 773 14.729 3 0.423 15.152 15616.0 16064.0 0.0 8645.5 304320.0 154566.3 70912.0 35858.5 21248.0 2361.2 777 14.788 3 0.423 15.212 13440.0 14144.0 0.0 7168.9 308544.0 193178.0 70912.0 35858.5 21248.0 2361.2 781 14.840 3 0.423 15.263 12928.0 13248.0 0.0 7200.9 310208.0 158366.9 70912.0 35858.5 21248.0 2361.2 785 14.870 3 0.423 15.294
オプションや出力項目はこちらを見ていただくとして…
http://java.sun.com/javase/ja/6/docs/ja/technotes/tools/share/jstat.html
「-t」オプションを付けるとJavaVMが起動してからの時間を出してくれるのですが、
$ jstat -gc -t 3835 1000 Timestamp S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 269.1 19584.0 19520.0 1997.8 0.0 298816.0 0.0 70912.0 49258.5 21248.0 2361.2 990 17.947 3 0.423 18.370 270.2 18368.0 18240.0 2267.5 0.0 300096.0 77625.2 70912.0 49258.5 21248.0 2361.2 994 17.976 3 0.423 18.400 271.2 16064.0 16448.0 6880.9 0.0 302464.0 166894.8 70912.0 49258.5 21248.0 2361.2 998 18.017 3 0.423 18.440
これだといつ取得したのか全然わからないので、自分はPerlのOne Linerとかで時間をくっつけるようにしてます。
$ jstat -gc 3835 1000 | perl -wn -e '$d = `date "+%Y/%m/%d %H:%M:%S"`; chomp $d; print "$d | $_"' 2012/06/23 20:30:24 | S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 2012/06/23 20:30:24 | 18816.0 19328.0 0.0 4373.9 299264.0 114667.8 107072.0 85039.6 21248.0 2361.5 2637 43.167 5 0.668 43.835 2012/06/23 20:30:25 | 19584.0 9472.0 0.0 9436.4 297472.0 113238.5 107072.0 85039.6 21248.0 2361.5 2641 43.226 5 0.668 43.894 2012/06/23 20:30:26 | 18240.0 17856.0 0.0 11290.1 299712.0 212660.6 107072.0 85039.6 21248.0 2361.5 2645 43.274 5 0.668 43.942
jmap [option] [PID]
ヒープダンプやヒープのヒストグラムを取得できるコマンドです。オプションを含めて、詳しい使い方はこちら。
http://java.sun.com/javase/ja/6/docs/ja/technotes/tools/share/jmap.html
ヒープダンプは、以下のようにして取得します。
$ jmap -dump:live,format=b,file=heap.dmp 3835 Dumping heap to /xxxxx/perf_mon/heap.dmp ... Heap dump file created
「-dump:live」で現在生存しているオブジェクトのみを、format=bでバイナリ形式でダンプします。出力ファイルはfile=[ファイル名]で指定します。取得したダンプファイルはjhatとかで見ることができますが、ヒープサイズがギガバイトクラスになると、ちょっと取得が難しくなりますね…。
ヒープのヒストグラムは、以下のようにして取得します。
$ jmap -histo:live 3835 num #instances #bytes class name ---------------------------------------------- 1: 21822 1001016 [C 2: 5232 716288 <methodKlass> 3: 21801 697632 java.lang.String 4: 5232 627456 <constMethodKlass> 5: 347 394304 <constantPoolKlass> 6: 313 250144 <constantPoolCacheKlass> 7: 347 249968 <instanceKlassKlass> 8: 313 97480 [Ljava.lang.Object; 9: 419 79792 [B 10: 407 49472 java.lang.Class 11: 560 36568 [[I 12: 515 32504 [S 13: 348 27320 [I 14: 43 24768 <objArrayKlassKlass> 15: 28 7736 <methodDataKlass> 16: 8 4608 <typeArrayKlassKlass> 17: 110 3520 java.util.Hashtable$Entry 18: 11 2288 <klassKlass> 19: 61 1952 java.util.concurrent.ConcurrentHashMap$HashEntry 20: 52 1904 [Ljava.lang.String; 〜省略〜 161: 1 16 sun.misc.Launcher$Factory 162: 1 16 java.nio.charset.CoderResult$1 163: 1 16 java.util.Collections$EmptyList 164: 1 16 java.util.Hashtable$EntrySet 165: 1 16 java.nio.charset.CoderResult$2 Total 58719 4334552
結果は、標準出力に吐き出されます。
「-histo」オプションでヒープヒストグラムを、liveを付けると現在生存中のオブジェクトのみを出力します。なお、liveを付けると副作用として強制的にFull GCがかかるため、外部からFull GCをかけるトリガーとしても有効です。ま、JMXが使えるのならその副作用自体はいらないかもしれませんが。
なお、注意点としてはjmapを実行すると、結果が取得できるまではJavaアプリ本体の動きが停止します。特に、ヒープダンプなんて取ろうものなら、ヒープのサイズに比例して停止時間が伸びるので、停止を許容できる時間内でダンプを取得できるかはかなり微妙です…。そもそも、ギガバイトクラスのヒープダンプを取得して、それを解析できるのか?って話もありますが。
あと、「-permstat」でパーマネント領域の統計情報を取得することもできます。
kill -3 [PID] または kill -QUIT [PID] *Windowsの場合はCtrl+Break
OS標準コマンドですが、これでスレッドダンプを取得することができます。なんかアプリの応答がないような時とか、現在どの辺りを処理しているかを見たい時に重宝します。割と便利なコマンドです。
実行例。
2012-06-23 20:45:42 Full thread dump Java HotSpot(TM) 64-Bit Server VM (23.1-b03 mixed mode): "Attach Listener" daemon prio=10 tid=0x00007f5b24001000 nid=0x10d3 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "DestroyJavaVM" prio=10 tid=0x00007f5b50008800 nid=0xefc waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Spare-Thread-9" prio=10 tid=0x00007f5b50118800 nid=0xf13 waiting on condition [0x00007f5b47dfc000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at SpareThread.run(Monitor.java:56) 〜省略〜 "Busy-Thread-2" prio=10 tid=0x00007f5b50106000 nid=0xf09 runnable [0x00007f5b4c8f7000] java.lang.Thread.State: RUNNABLE at BusyThread.run(Monitor.java:32) 〜省略〜 "Signal Dispatcher" daemon prio=10 tid=0x00007f5b500e3800 nid=0xf02 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Finalizer" daemon prio=10 tid=0x00007f5b50096800 nid=0xf01 in Object.wait() [0x00007f5b544e0000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000c2217b90> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135) - locked <0x00000000c2217b90> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:177) "Reference Handler" daemon prio=10 tid=0x00007f5b50094800 nid=0xf00 in Object.wait() [0x00007f5b545e1000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000c22178e0> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:503) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133) - locked <0x00000000c22178e0> (a java.lang.ref.Reference$Lock) "VM Thread" prio=10 tid=0x00007f5b5008d000 nid=0xeff runnable "GC task thread#0 (ParallelGC)" prio=10 tid=0x00007f5b50016800 nid=0xefd runnable "GC task thread#1 (ParallelGC)" prio=10 tid=0x00007f5b50018000 nid=0xefe runnable "VM Periodic Task Thread" prio=10 tid=0x00007f5b500f5800 nid=0xf06 waiting on condition JNI global references: 135 Heap PSYoungGen total 321024K, used 173113K [0x00000000eb600000, 0x0000000100000000, 0x0000000100000000) eden space 304896K, 56% used [0x00000000eb600000,0x00000000f5ede700,0x00000000fdfc0000) from space 16128K, 1% used [0x00000000fdfc0000,0x00000000fdff0000,0x00000000fef80000) to space 16640K, 0% used [0x00000000fefc0000,0x00000000fefc0000,0x0000000100000000) ParOldGen total 160640K, used 100344K [0x00000000c2200000, 0x00000000cbee0000, 0x00000000eb600000) object space 160640K, 62% used [0x00000000c2200000,0x00000000c83fe158,0x00000000cbee0000) PSPermGen total 21248K, used 2361K [0x00000000bd000000, 0x00000000be4c0000, 0x00000000c2200000) object space 21248K, 11% used [0x00000000bd000000,0x00000000bd24e5e8,0x00000000be4c0000)
GCスレッドを含めて、JavaVMに存在するすべてのスレッドのダンプが出力されます。また、結果は、killコマンドが適用されたJavaアプリケーションの標準出力に吐き出されます。よって、コンソールを失っているプログラムだと、結果が取得できない可能性があります。Tomcatの場合は、catalina.outに出力されます。
なお、killコマンドでスレッドダンプを取得した場合は、最後にヒープの統計情報が出力されます。
jstack [option] [PID]
kill -QUITと同じく、スレッドダンプを取得するコマンドです。場合によっては、kill -QUITよりも役に立つことがあります。
http://java.sun.com/javase/ja/6/docs/ja/technotes/tools/share/jstack.html
実行例。
$ jstack 3835 2012-06-23 20:50:44 Full thread dump Java HotSpot(TM) 64-Bit Server VM (23.1-b03 mixed mode): "Attach Listener" daemon prio=10 tid=0x00007f5b24001000 nid=0x10d3 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "DestroyJavaVM" prio=10 tid=0x00007f5b50008800 nid=0xefc waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE 〜省略〜 "VM Thread" prio=10 tid=0x00007f5b5008d000 nid=0xeff runnable "GC task thread#0 (ParallelGC)" prio=10 tid=0x00007f5b50016800 nid=0xefd runnable "GC task thread#1 (ParallelGC)" prio=10 tid=0x00007f5b50018000 nid=0xefe runnable "VM Periodic Task Thread" prio=10 tid=0x00007f5b500f5800 nid=0xf06 waiting on condition JNI global references: 135
kill -QUITで取得した場合との違いは、以下になります。
- 結果は、jstackコマンドを実行した側に出力される
- ヒープの統計情報は出力されない
- 「-m」オプションで、ネイティブC/C++フレームの情報も取得可能
バッチプログラムなどで標準出力を捨ててしまっている場合などは、kill -QUITでは結果を見ることができないのでけっこう重宝します。
なお、kill -QUITもjstackも取得する時にはやっぱり対象のJavaアプリケーションが一瞬停止します。まあ、ホントに瞬間なのであまり問題はありませんが、「-m」でネイティブ領域を含めた場合はかなりの時間がかかるので、その場合だけ注意ですね。
【オマケ:CPUを消費しているスレッドをjstackの結果と紐付ける】
Linuxの場合の例です。やり方自体は、けっこう最近知りました。
まず、psコマンドでスレッド単位に出力した上で、CPUの使用率も取っておきます。
$ ps auxww -L USER PID LWP %CPU NLWP %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 1 0.0 1 0.0 24448 2416 ? Ss 19:21 0:03 /sbin/init root 2 2 0.0 1 0.0 0 0 ? S 19:21 0:00 [kthreadd] root 3 3 0.0 1 0.0 0 0 ? S 19:21 0:00 [ksoftirqd/0] 〜省略〜 xxxxx 3835 3846 0.0 26 13.4 2263124 543200 pts/3 Sl+ 20:18 0:02 java Monitor xxxxx 3835 3847 32.6 26 13.4 2263124 543200 pts/3 Rl+ 20:18 13:31 java Monitor xxxxx 3835 3848 32.5 26 13.4 2263124 543200 pts/3 Rl+ 20:18 13:31 java Monitor xxxxx 3835 3849 32.5 26 13.4 2263124 543200 pts/3 Rl+ 20:18 13:30 java Monitor xxxxx 3835 3850 0.0 26 13.4 2263124 543200 pts/3 Sl+ 20:18 0:00 java Monitor xxxxx 3835 3851 0.0 26 13.4 2263124 543200 pts/3 Sl+ 20:18 0:00 java Monitor xxxxx 3835 3852 0.0 26 13.4 2263124 543200 pts/3 Sl+ 20:18 0:00 java Monitor 〜省略〜
この場合、スレッドのLWPのうち、3847、3848、3849が主にCPUを消費していることがわかります。この3つを16進数に変換します。16進数に変換する方法は何でもいいですが。
$ printf '%#x\n' 3847 0xf07 $ printf '%#x\n' 3848 0xf08 $ printf '%#x\n' 3849 0xf09
ここで、jstackやkill -QUITでスレッドダンプを取得してみます。
$ jstack 3835 2012-06-23 21:03:34 Full thread dump Java HotSpot(TM) 64-Bit Server VM (23.1-b03 mixed mode): "Attach Listener" daemon prio=10 tid=0x00007f5b24001000 nid=0x10d3 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "DestroyJavaVM" prio=10 tid=0x00007f5b50008800 nid=0xefc waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE 〜省略〜 "Spare-Thread-0" prio=10 tid=0x00007f5b50108800 nid=0xf0a waiting on condition [0x00007f5b4c7f6000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at SpareThread.run(Monitor.java:56) "Busy-Thread-2" prio=10 tid=0x00007f5b50106000 nid=0xf09 sleeping[0x00007f5b4c8f7000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at BusyThread.run(Monitor.java:36) "Busy-Thread-1" prio=10 tid=0x00007f5b50104000 nid=0xf08 runnable [0x00007f5b4c9f8000] java.lang.Thread.State: RUNNABLE at BusyThread.run(Monitor.java:34) "Busy-Thread-0" prio=10 tid=0x00007f5b50102000 nid=0xf07 runnable [0x00007f5b4caf9000] java.lang.Thread.State: RUNNABLE at BusyThread.run(Monitor.java:34) "Service Thread" daemon prio=10 tid=0x00007f5b500eb000 nid=0xf05 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE 〜省略〜 JNI global references: 135
ここで、「nid=」箇所に注目してみます。
"Busy-Thread-2" prio=10 tid=0x00007f5b50106000 nid=0xf09 sleeping[0x00007f5b4c8f7000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at BusyThread.run(Monitor.java:36) "Busy-Thread-1" prio=10 tid=0x00007f5b50104000 nid=0xf08 runnable [0x00007f5b4c9f8000] java.lang.Thread.State: RUNNABLE at BusyThread.run(Monitor.java:34) "Busy-Thread-0" prio=10 tid=0x00007f5b50102000 nid=0xf07 runnable [0x00007f5b4caf9000] java.lang.Thread.State: RUNNABLE at BusyThread.run(Monitor.java:34)
先ほど取得した、スレッドのLWPと一致することが確認できましたね。
スレッドのCPU使用率順にソートして表示するなら、こんな感じでいいかな。
$ ps auxww -L | sort -n -k 4
また、スレッドのIDとjstackの結果の紐付け方ですが、16進数に変換するのではなくてPerl One Linerとかで
「nid=」の部分を直接10進数に戻しても良いかなと。
$ jstack 3835 | perl -wp -e 's!(.+)nid=0x([a-z0-9]+)(.+)!print "$1nid=" . hex("$2") . "$3"!e' 2012-06-23 21:03:34 Full thread dump Java HotSpot(TM) 64-Bit Server VM (23.1-b03 mixed mode): "Attach Listener" daemon prio=10 tid=0x00007f5b24001000 nid=4307 waiting on condition [0x0000000000000000]1 java.lang.Thread.State: RUNNABLE "DestroyJavaVM" prio=10 tid=0x00007f5b50008800 nid=3836 waiting on condition [0x0000000000000000]1 java.lang.Thread.State: RUNNABLE 〜省略〜 "Spare-Thread-0" prio=10 tid=0x00007f5b50108800 nid=3850 waiting on condition [0x00007f5b4c7f6000]1 java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at SpareThread.run(Monitor.java:56) "Busy-Thread-2" prio=10 tid=0x00007f5b50106000 nid=3849 sleeping[0x00007f5b4c8f7000]1 java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at BusyThread.run(Monitor.java:36) "Busy-Thread-1" prio=10 tid=0x00007f5b50104000 nid=3848 runnable [0x00007f5b4c9f8000]1 java.lang.Thread.State: RUNNABLE at BusyThread.run(Monitor.java:34) "Busy-Thread-0" prio=10 tid=0x00007f5b50102000 nid=3847 runnable [0x00007f5b4caf9000]1 java.lang.Thread.State: RUNNABLE at BusyThread.run(Monitor.java:34) "Service Thread" daemon prio=10 tid=0x00007f5b500eb000 nid=3845 runnable [0x0000000000000000]1 java.lang.Thread.State: RUNNABLE 〜省略〜 JNI global references: 135