まとめ:
- jfrファイルをjavaコードから読み込むときは、jfrの監視対象javaプロセスのバージョンに気をつける必要がある。
- JMCから開く場合には特に気にする必要はない
- ローカルでざっくり見た結果だから保証はしないよ
Java Flight Recorder (JFR)とは
Java Flight Recorder (JFR)は実行中のJavaプロセスのプロファイルを取得するツールです。
現在のところは本番環境で使用するには商業利用ライセンスが必要ですが、本番ではない環境でのパフォーマンス問題調査に無くてはならないものです。
注意:
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で開く場合には、特に問題は発生しません。