Javaコード内でjfrファイルを読もうとすると互換性に気をつけないといけない話

まとめ:

  • jfrファイルをjavaコードから読み込むときは、jfrの監視対象javaプロセスのバージョンに気をつける必要がある。
  • JMCから開く場合には特に気にする必要はない
  • ローカルでざっくり見た結果だから保証はしないよ

Java Flight Recorder (JFR)とは

Java Flight Recorder (JFR)は実行中のJavaプロセスのプロファイルを取得するツールです。
現在のところは本番環境で使用するには商業利用ライセンスが必要ですが、本番ではない環境でのパフォーマンス問題調査に無くてはならないものです。

Java Flight Recorderについて

注意:
Java Flight Recorderを本番で使用するには、商用ライセンスが必要です。

将来的にJava Mission Control (JMC)がオープンソースへ移行することでJFRの商用ライセンス縛りも外れるのではないか...と言われていますが、Oracleからその辺あんまり詳しいアナウンスは出ていないのでよくわかりません。
JMC Open Sourced! – Marcus Hirt


で、このJFRで生成される*.jfrファイルは、通常はJMCから読み込むのですが、実はJavaコード中で読み込むことも可能です。しかし...というのが本題。

jfrファイルを読み込む方法

1. jfr.jarを使用する (java8まで)

${JDK_HOME}/jre/lib/jfr.jar にclasspathを通すと、jfrファイルを読み込むAPIを参照できます。
読み込みはざっくりこんな感じ。

非公開APIなので注意すること。
あと、JFRファイルはデフォルトで圧縮がかかっているようなので、読み込む前に解凍処理を忘れずに。

2. java jfr APIを使用する

Java9以降、jfrを読み込むためのAPIが公開APIとして実装されました。
jdk.jfr (Java SE 9 & JDK 9 )

読み込みはざっくりこんな感じ

バージョン互換問題

Java9以降であれば公開APIを使えば良いんじゃないかと思っていましたが、思わぬ罠がありました。

測定条件は以下の通り

  • java1.8 (1.8.172)、java9 (9.0.1) 、java10 (10.0.1)でjavaアプリ(apache jmeter)を立ち上げ、JFR取得対象とする
  • java1.8、java9、java10それぞれのjcmdを使用して、上のそれぞれのアプリについてのJFRを取得する
  • 上で貼ったコードをjava9環境で実行する(java8 のjfr.jarへclasspathを通した状態)

結果は以下の通り

取得対象Java version jcmdのJava version jfr.jarによる読み込み jfr APIによる読み込み
java1.8 java1.8 問題なし エラー1
java1.8 java9 問題なし エラー1
java1.8 java10 問題なし エラー1
java 9 java1.8 エラー2 問題なし
java 9 java9 エラー2 問題なし
java 9 java10 エラー2 問題なし
java 10 java1.8 エラー3 問題なし
java 10 java9 エラー3 問題なし
java 10 java10 エラー3 問題なし

つまり、jfr.jarはjava9以降のプロセスから取得したjfrを読めないし、一方でjfr APIはjava1.8プロセスから取得したjfrを読めないという話ですね。そして、取得対象のバージョンには依存するものの取得する側のjcmd(多分JMCも)のバージョンには依存しない。
ローカル環境でしか検証していないけれど多分あってるはず。

エラー1の内容は以下の通り。

java.io.IOException: File version 0.9. Only Java Flight Recorder files of version 1.x can be read by this JDK.
	at jdk.jfr/jdk.jfr.internal.consumer.ChunkHeader.<init>(ChunkHeader.java:56)
	at jdk.jfr/jdk.jfr.internal.consumer.ChunkHeader.<init>(ChunkHeader.java:38)
	at jdk.jfr/jdk.jfr.consumer.ChunkParser.<init>(ChunkParser.java:35)
	at jdk.jfr/jdk.jfr.consumer.RecordingFile.findNext(RecordingFile.java:181)
	at jdk.jfr/jdk.jfr.consumer.RecordingFile.<init>(RecordingFile.java:63)
	at jdk.jfr/jdk.jfr.consumer.RecordingFile.readAllEvents(RecordingFile.java:168)
	at jfr_example.Jdk9JfrReader.read(Jdk9JfrReader.java:31)
	at jfr_example.Main.main(Main.java:23)

エラー2

java.lang.RuntimeException: oracle.jrockit.jfr.parser.ParseException: Bad descriptor section, id=65536
	at oracle.jrockit.jfr.parser.Parser$1.hasNext(Parser.java:165)
	at jfr_example.Jdk8JfrReader.read(Jdk8JfrReader.java:31)
	at jfr_example.Main.main(Main.java:18)
Caused by: oracle.jrockit.jfr.parser.ParseException: Bad descriptor section, id=65536
	at oracle.jrockit.jfr.parser.ChunkParser.readDescriptors(ChunkParser.java:555)
	at oracle.jrockit.jfr.parser.ChunkParser.begin(ChunkParser.java:230)
	at oracle.jrockit.jfr.parser.ChunkParser.<init>(ChunkParser.java:95)
	at oracle.jrockit.jfr.parser.Parser.next(Parser.java:124)
	at oracle.jrockit.jfr.parser.Parser$1.hasNext(Parser.java:163)
	... 2 more

エラー3

java.lang.RuntimeException: java.nio.BufferUnderflowException
	at oracle.jrockit.jfr.parser.Parser$1.hasNext(Parser.java:165)
	at jfr_example.Jdk8JfrReader.read(Jdk8JfrReader.java:31)
	at jfr_example.Main.main(Main.java:18)
Caused by: java.nio.BufferUnderflowException
	at java.base/java.nio.Buffer.nextGetIndex(Buffer.java:634)
	at java.base/java.nio.DirectByteBuffer.getInt(DirectByteBuffer.java:686)
	at oracle.jrockit.jfr.parser.MappedFLRInput.getInt(MappedFLRInput.java:79)
	at oracle.jrockit.jfr.parser.ChunkParser.readDescriptors(ChunkParser.java:552)
	at oracle.jrockit.jfr.parser.ChunkParser.begin(ChunkParser.java:230)
	at oracle.jrockit.jfr.parser.ChunkParser.<init>(ChunkParser.java:95)
	at oracle.jrockit.jfr.parser.Parser.next(Parser.java:124)
	at oracle.jrockit.jfr.parser.Parser$1.hasNext(Parser.java:163)
	... 2 more

なので、JFRをjavaから読む何かを書くときにはこの辺を気をつけないと「一年前に取得したJFRファイルが読めねー!」って事になるので注意が必要です。
あと、jfrファイルをJavaコードではなくJMCで開く場合には、特に問題は発生しません。