薄いブログ

技術の雑多なことを書く場所

JavaにおけるHeapのClassHistogramを取得する複数の方法とその違い

TL;DR

方法 Full GCの有無 タイミング 条件
JFR の jdk.ObjectCount あり 定期的 JFRが使える
-XX:+PrintClassHistogram あり シグナル受信時 JDK 1.4.2~
jmap -histo:live あり 実行時 JDK 1.4.2~
jcmd GC.class_histogram あり 実行時 JDK 7~
jmap -histo なし 実行時 JDK 1.4.2~
jcmd GC.class_histogram -all なし 実行時 JDK 7~
-XX:+PrintClassHistogramBeforeFullGC なし Full GC JDK 6(?)~8
-XX:+PrintClassHistogramAfterFullGC なし Full GC JDK 6(?)~8
-Xlog:classhisto*=trace なし Full GC 前後 JDK 9~

はじめに

JavaにおけるHeapのClassHistogramを取得する方法が複数種類あり、それぞれの違いがわからなかったので調べた。

実装の詳細については f3ca0cab75f2faf9ec88f7a380490c9589a27102 時点の openjdk/jdk の情報を元にしている。

github.com

過去のバージョン時点でも同様の挙動であるとは限らない。

OpenJDK から得た情報なので 6 より前の情報は正確でない可能性がある。

-XX:+PrintClassHistogram を使う

Java 1.4.2 からあるオプションで CONTROL + BREAK を外部から送ったタイミングでログに出力される。 生存しているオブジェクトのみを出力するために Full GC をトリガーする。

Signal Dispatcher スレッド (signal_thread_entry) -> VMThread::execute(VM_GC_HeapInspection)

VM_GC_HeapInspection が実体。

jmap -histo を使う

Java 1.4.2 からあるツールで VM にアタッチして heapinspect を実行する。

Attach Listener スレッド(attach_listener_thread_entry) -> heap_inspection -> VMThread::execute(VM_GC_HeapInspection) という流れで処理される。

VM_GC_HeapInspection が実体。 live というオプションを付けると Full GC をトリガーする。

jcmd GC.class_histogram を使う

Java 7 からあるツールで VM にアタッチして jcmd を実行する。

Attach Listener スレッド(attach_listener_thread_entry) -> jcmd -> DCmd::parse_and_execute -> ClassHistogramDCmd::execute -> VMThread::execute(VM_GC_HeapInspection) という流れで処理される。

VM_GC_HeapInspection が実体。

-all をつけると Full GC をトリガーしない。

-XX:+PrintClassHistogramBeforeFullGC -XX:+PrintClassHistogramAfterFullGC を使う

Full GC が発生する/したタイミングで出力される。 STW の時間が増加するが自動で出力される。

ClassHistogram は Full GC の前後で見たいことが多いので一番良さそう。

-Xlog:classhisto*=trace

-XX:+PrintClassHistogramBeforeFullGC-XX:+PrintClassHistogramAfterFullGCJava 9 からの JEP 271 で削除され -Xlog:classhisto*=trace に変更された。 基本的に同じもの。 どちらか片方のみを有効にすることができなくなった。

JFR の jdk.ObjectCount を 使う

これを JFR で有効にすると VM_GC_SendObjectCountEvent(VM_GC_HeapInspection) によって毎回 Full GC がトリガーされる。 jfr configure で gc の項目を all にすると jdk.ObjectCount が有効な jfc が生成される。

VM_GC_HeapInspection

VM_GC_HeapInspection は safepoint で評価されるので STW が Full GC の有無に関わらず発生する。

まとめ

方法 Full GCの有無 タイミング 条件
JFR の jdk.ObjectCount あり 定期的 JFRが使える
-XX:+PrintClassHistogram あり シグナル受信時 JDK 1.4.2~
jmap -histo:live あり 実行時 JDK 1.4.2~
jcmd GC.class_histogram あり 実行時 JDK 7~
jmap -histo なし 実行時 JDK 1.4.2~
jcmd GC.class_histogram -all なし 実行時 JDK 7~
-XX:+PrintClassHistogramBeforeFullGC なし Full GC JDK 6(?)~8
-XX:+PrintClassHistogramAfterFullGC なし Full GC JDK 6(?)~8
-Xlog:classhisto*=trace なし Full GC 前後 JDK 9~

余談1

https://github.com/openjdk/jdk/blob/f3ca0cab75f2faf9ec88f7a380490c9589a27102/src/jdk.jcmd/share/classes/sun/tools/jmap/JMap.java#L198-L199

// inspectHeap is not the same as jcmd GC.class_histogram

というコメントがあるが jmap -histo で実行する inspectheap は jcmd GC.class_histogram と同様の処理をする。 jmap -histo と等価なのは jcmd GC.class_histogram -all だという主張なら理解できる。

余談2

https://docs.oracle.com/en/java/javase/19/docs/specs/man/java.html

-XX:+PrintClassHistogram Setting this option is equivalent to running the jmap -histo command, or the jcmd pid GC.class_histogram command, where pid is the current Java process identifier.

と書いてあるが -XX:+PrintClassHistgoram と厳密に等価なのは jmap -histo:livejcmd GC.class_histogram だと思われる。

jmap だけデフォルトが Full GC をトリガーしないところため注意が必要だと思われる。

参考リンク