この記事は NTT コムウェア Advent Calendar 2023 10日目の記事です。
はじめに
こんにちは、 NTT コムウェア株式会社の坂本統です。
私は普段 Java システムの技術サポートをお仕事にしています。特に Java 実行・開発環境ソフトウェア(JRE/JDK)である OpenJDK を専門として Java 仮想マシン(JVM)のトラブルシューティングを得意としています。
この記事では近頃 Java 界隈で注目されている GraalVM Native Image トラブルシューティングことはじめとして、Native Image から JVM 関連情報を取得する方法とその情報を参照する方法についてご紹介いたします。
GraalVM Native Image とは
GraalVM は Oracle 社が主導して開発している OpenJDK ベースの Java 実行開発環境ソフトウェアで、Java 実行基盤として互換性を持つ他に以下の3つの強力な機能を持ち、次世代 Java 高速実行基盤として期待されています。
- 高性能 Just-In-Time(JIT) コンパイラ
- Ahead-Of-Time(AOT) コンパイラによる高速ネイティブ実行(Native Image)
- 多言語プログラミング対応(Truffle)
このうち Native Image 機能 は Java アプリケーションを高速で軽量な実行ファイル(Native Image)に変換する機能です。ざっくり伝えると Java コードの解析によって同等の C++ コードに変換してコンパイルします。この機能により生成された実行ファイルは Native Image と呼ばれ、通常の Java アプリケーションと比較して高速起動かつ低メモリ消費のメリットを持ちます。またアプリケーションに必要な最小限の Java コードしか含めないためファイルサイズの縮小やセキュリティホールの極小化といったメリットもあります。
さて話が変わりますが OpenJDK や GraalVM といった多くの JRE/JDK は HotSpot と言う名前の JVM を使用します。JVM のトラブルシューティングでは HotSpot VM が備える機能を使用して解析に必要な情報、例えば GC ログやフライトレコードなどを取得します。一方 Native Image は軽量な Substrate VM を使用します。これは HotSpot VM で取得できた情報が Substrate VM からは取得不可であることや、取得可能であっても取得手順が異なることを意味します。
実際のところ、GraalVM の開発が進むことでほぼ同じ情報を取得できる様になりました。ただし取得方法や情報の内容にはいくつかの差異があります。前置きが長くなりましたがこの記事では HotSpot VM と比較した、 Substrate VM(Native Image)) から情報を取得する方法とその情報を参照する方法について紹介いたします。
JVM から取得可能な情報
本記事でまとめる JVM から取得可能な情報は以下の通りです。
情報 | 主な取得目的 |
---|---|
バージョン | 実行環境の確認 |
JVM オプション | アプリケーション設定の確認 |
GC ログ(Serial GC) | GC チューニング |
スレッドダンプ | ハングアップ解析 |
ヒープダンプ | OutOfMemoryError 解析 |
フライトレコード | アプリケーションプロファイリング |
エラーレポート | クラッシュ解析 |
コアダンプ | デバッグ |
調査環境
調査環境は以下の通りです。
項目 | 詳細 | 備考 |
---|---|---|
OS | Ubuntu 22.04 | |
Java 実行環境 | GraalVM for JDK 21 Community | オープンソース版 |
本記事では Linux 環境における Native Image 機能の情報をまとめています。Windows 環境ではいくつかの制限により同様の結果とならない可能性があることに注意して下さい。
取得可能な情報や内容は GraalVM のバージョンによって異なります。本記事では GraalVM for JDK 21 に基づく情報を記載しています。お手元で試す場合は 21 以降のバージョンを使用することを推奨します。
検証用アプリケーションとビルド手順
情報の取得方法や参照方法を説明する前にまず簡単に Native Image の使用方法を紹介します。
はじめに公式の GitHub リポジトリから GraalVM for JDK 21 Community を取得し、 PATH
や JAVA_HOME
を設定して環境を整えて下さい。
SDKMAN を使用すると簡単に取得・設定できます。
次に検証用に簡単な Java アプリケーションを作成しましょう。ここでは何もせず待機し続けるだけの Sleep
と意図的に OutOfMemoryError を引き起こす OutOfMemoryError
を作成します。
class Sleep {
public static void main(String[] args) throws Exception {
Thread.sleep(10000000);
}
}
public class OutOfMemoryError {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
while(true){
sb.append("OOME will occur.");
}
}
}
最後に Native Image を生成します。はじめに javac
で Java アプリケーションをコンパイルした後、GraalVM に付属の native-image
でビルドします。
$ javac MyApp.java
$ native-image MyApp
何もトラブルがなければ native-image
を実行した後に実行ファイル(Native Image)が生成されているはずです。Native Image は元となるクラスファイルや JAR ファイルの名前部分がすべて小文字表記になったファイル名で生成されます。以下の例では myapp
が該当します。
$ ls
myapp MyApp.java MyApp.class
実際のところいくつかの情報の取得には別途 native-image
へビルドオプション指定が必要ですが、詳しくは後述します。
(参考)Spring Petclinic
フライトレコードなどの長時間の情報を参照する時は前述の様なシンプルなアプリケーションでは十分な情報が記録されません。そのため Web アプリケーションの様な複雑なアプリケーションが欲しくなります。その場合は Spring Petclinic をお勧めします。これは Sprin Boot を使用して実装された架空の動物病院のサイトを模した Web アプリケーションのサンプルです。Native Image をサポートしていますので簡単にビルドできます。
詳しい手順は省略しますが以下のいずれかのコマンドを実行することでビルドできます。ビルドにはやや時間がかかるのでコーヒーでも淹れてゆっくりと待ちましょう。
$ mvn -Pnative native:compile
$ gradle nativeCompile
Spring Boot アプリケーションの Native Image ビルドに関する詳細は GraalVM Native Image Support を参照して下さい。
情報取得方法および参照方法
以降は Native Image から各種情報を取得する方法と参照方法についてまとめます。
バージョン
トラブルシューティングを始めるにあたり、どのバージョンの JRE/JDK で動作しているかを把握する必要があるでしょう。
通常の Java 実行環境でバージョンを確認するには java -version
を実行しますが、 Native Image の場合は実行ファイルそのものに JVM が含まれるため、strings
コマンドを使用してバージョンを含めた文字列を抽出します。バージョン情報だけ抽出するには com.oracle.svm.core.VM
で grep
して下さい。以下は実行例です。ここで <native_image>
は生成した Native Image 自身か、そのファイル名を指します。
$ strings <native_image> | grep com.oracle.svm.core.VM
com.oracle.svm.core.VMInspectionOptions_OptionDescriptors
com.oracle.svm.core.VMInspectionOptions_OptionDescriptors
com.oracle.svm.core.VMInspectionOptions
com.oracle.svm.core.VM
com.oracle.svm.core.VM=21+35
com.oracle.svm.core.VM.Java.Version=21
...省略
この例では com.oracle.svm.core.VM.Java.Version=21
から Java バージョンは 21
であること、com.oracle.svm.core.VM=21+35
から GraalVM のバージョンとビルド番号は 21+35
であることが分かります。
JVM オプション
実行中のアプリケーションの設定を把握するために指定されている JVM オプションを確認したい場合があります。
これは通常の Java アプリケーションと同様に ps aux | grep <native_image_name>
コマンドを実行することで確認できます。
$ ps aux | grep <native_image>
user 1327261 2.5 0.3 1828040 213956 pts/0 Sl+ 17:26 0:00 /path/to/native_image -XX:+PrintGC -XX:+PrintGCSummary
...省略
この例では GC 関連の JVM オプション -XX:+PrintGC -XX:+PrintGCSummary
を指定していることが分かります。
Native Image に -XX:+PrintFlags=
を指定して実行すると指定可能な JVM オプション一覧を確認できます。
$ <native_image> -XX:PrintFlags=
-XX:ActiveProcessorCount=-1 Overwrites the available number of processors
provided by the OS. Any value <= 0 means using the processor count from
the OS.
-XX:±AutomaticReferenceHandling Determines if the reference handling is executed
automatically or manually. Default: + (enabled).
...省略
GC ログ(Serial GC)
Native Image に以下の JVM オプションを指定することで GC ログが標準エラー出力されます。基本となる JVM オプションは -XX:+PrintGC
と -XX:+VerboseGC
です。その他のオプションはこれら2つのオプションが指定されていないと有効化できない場合があります。詳細な使用方法は前述の -XX:+PrintFlags=
で確認して下さい。
JVM オプション名 | 内容 |
---|---|
-XX:±PrintGC | 各 GC 実行後の GC 情報を表示 |
-XX:±VerboseGC | 各 GC 実行前後の詳細な情報を表示 |
-XX:±PrintGCSummary | アプリケーション終了後の GC 情報サマリを表示 |
-XX:±PrintGCTimes | 各 GC のフェーズごとの実行時間を表示 |
-XX:±TraceHeapChunks | GC 実行中のヒープチャンクを追跡 |
以下は前述の OutOfMemoryError
を使用して -XX:+PrintGC
と -XX:VerboseGC
を指定した場合の出力例です。またヒープ領域を制限するために -Xmx100m -Xms100m -Xmn30m
も指定しています。
$ ./outofmemoryerror -Xmx100m -Xms100m -Xmn30m -XX:+PrintGC -XX:+VerboseGC
[0.059s] GC(0) Using Serial GC
[0.059s] GC(0) Memory: 64026M
[0.059s] GC(0) Heap policy: adaptive
[0.059s] GC(0) Maximum young generation size: 30M
[0.059s] GC(0) Maximum heap size: 100M
[0.059s] GC(0) Minimum heap size: 100M
[0.059s] GC(0) Aligned chunk size: 512K
[0.059s] GC(0) Large array threshold: 128K
[0.059s] GC(0) Collect on allocation
[0.060s] GC(0) Eden: 37.85M->0.00M
[0.060s] GC(0) Survivor: 0.00M->0.00M
[0.060s] GC(0) Old: 0.00M->18.50M
[0.060s] GC(0) Free: 0.00M->2.00M
[0.060s] GC(0) Full GC (Collect on allocation) 37.85M->18.50M 1.313ms
[0.104s] GC(1) Collect on allocation
[0.105s] GC(1) Eden: 36.00M->0.00M
[0.105s] GC(1) Survivor: 0.00M->0.00M
[0.105s] GC(1) Old: 18.50M->36.50M
[0.105s] GC(1) Free: 2.00M->2.00M
[0.105s] GC(1) Full GC (Collect on allocation) 54.50M->36.50M 1.265ms
[0.189s] GC(2) Collect on allocation
[0.191s] GC(2) Eden: 72.00M->0.00M
[0.191s] GC(2) Survivor: 0.00M->0.00M
[0.191s] GC(2) Old: 36.50M->72.50M
[0.191s] GC(2) Free: 2.00M->2.00M
[0.191s] GC(2) Full GC (Collect on allocation) 108.50M->72.50M 2.502ms
...省略
これは GC 番号 0
から 2
の出力例です。-XX:+PrintGC
を指定することで [0.060s] GC(0) Full GC (Collect on allocation) 37.85M->18.50M 1.313ms
の様な一行が出力されます。この行から GC 種別とその理由、Java ヒープ使用量の推移、実行時間が分かります。その他の部分は -XX:VerboseGC
を指定することで出力される詳細なログです。
見ての通り Native Image(Substrate VM)が出力する GC ログは独自の形式です。そのため既存の GC ログ解析ツール(GCViewer など)が対応していないことに注意して下さい。
本記事ではオープンソースの GraalVM Community for JDK 21 を使用し、唯一選択可能な Serial GC 方式に基づく内容を記載しています。JDK バージョンが異なる場合や、プロプライエタリの Oracle GraalVM を使用して G1GC 方式を選択してビルドした場合は前述の説明と異なる点があることに注意して下さい。
スレッドダンプ
スレッドダンプを取得したい場合は Native Image 機能によるビルド時から設定が必要です。スレッドダンプ出力機能はデフォルトで Native Image に含まれません。そのため native-image
のビルドオプション -H:+DumpThreadStacksOnSignal
を指定してビルドする必要があります。
$ javac MyApp.java
$ native-image -H:+DumpThreadStacksOnSignal MyApp
-H:+DumpThreadStacksOnSignal は実験的(Experimental)なビルドオプションであるためビルド時に警告が出ます。
このビルドオプションを指定してビルドした Native Image はスレッドダンプ出力機能を持ちます。スレッドダンプを出力するには実行中のアプリケーションプロセスに対して QUIT
シグナルを送信して下さい。QUIT
シグナルを受信した Native Image はスレッドダンプを標準エラー出力します。
$ kill -QUIT <native_image_pid>
以下は Sleep
のスレッドダンプを出力した例です。
Full thread dump:
"Signal Dispatcher" #29 daemon thread=0x00007fd65c000b80 state=RUNNABLE
SP 0x00007fd668dfedd0 IP 0x000055f07d5c966a [image code]
com.oracle.svm.core.posix.headers.CSunMiscSignal.await(CSunMiscSignal.java)
SP 0x00007fd668dfedd0 IP 0x000055f07d5c966a [image code]
com.oracle.svm.core.posix.Util_jdk_internal_misc_Signal$SignalState.await(SunMiscSubstitutions.java:353)
SP 0x00007fd668dfedd0 IP 0x000055f07d5c966a [image code]
com.oracle.svm.core.posix.Util_jdk_internal_misc_Signal$DispatchThread.run(SunMiscSubstitutions.java:289)
SP 0x00007fd668dfee40 IP 0x000055f07d64dce4 [image code] java.lang.Thread.run(Thread.java:833)
SP 0x00007fd668dfee50 IP 0x000055f07d5e327a [image code]
com.oracle.svm.core.thread.PlatformThreads.threadStartRoutine(PlatformThreads.java:775)
SP 0x00007fd668dfee80 IP 0x000055f07d5cfb17 [image code]
com.oracle.svm.core.posix.thread.PosixPlatformThreads.pthreadStartRoutine(PosixPlatformThreads.java:203)
SP 0x00007fd668dfeea0 IP 0x000055f07d55e7ef [image code]
com.oracle.svm.core.code.IsolateEnterStub.PosixPlatformThreads_pthreadStartRoutine_cf47bd191b082a3631657d369b9255c828b9
a95c(IsolateEnterStub.java:0)
"Reference Handler" #28 daemon thread=0x00007fd664000b80 state=WAITING
SP 0x00007fd6697fed90 IP 0x000055f07d57cf56 [image code]
com.oracle.svm.core.genscavenge.HeapImpl.transitionToNativeThenAwaitPendingRefs(HeapImpl.java:569)
SP 0x00007fd6697fedc0 IP 0x000055f07d57d02b [image code]
com.oracle.svm.core.genscavenge.HeapImpl.waitForPendingReferenceList(HeapImpl.java:558)
SP 0x00007fd6697fedf0 IP 0x000055f07d57d113 [image code]
com.oracle.svm.core.genscavenge.HeapImpl.waitForReferencePendingList(HeapImpl.java:548)
SP 0x00007fd6697fee20 IP 0x000055f07d5977c0 [image code]
com.oracle.svm.core.heap.ReferenceInternals.waitForPendingReferences(ReferenceInternals.java:147)
SP 0x00007fd6697fee20 IP 0x000055f07d5977c0 [image code]
com.oracle.svm.core.heap.ReferenceHandlerThread.run(ReferenceHandlerThread.java:83)
SP 0x00007fd6697fee40 IP 0x000055f07d64dce4 [image code] java.lang.Thread.run(Thread.java:833)
SP 0x00007fd6697fee50 IP 0x000055f07d5e327a [image code]
com.oracle.svm.core.thread.PlatformThreads.threadStartRoutine(PlatformThreads.java:775)
SP 0x00007fd6697fee80 IP 0x000055f07d5cfb17 [image code]
com.oracle.svm.core.posix.thread.PosixPlatformThreads.pthreadStartRoutine(PosixPlatformThreads.java:203)
SP 0x00007fd6697feea0 IP 0x000055f07d55e7ef [image code]
com.oracle.svm.core.code.IsolateEnterStub.PosixPlatformThreads_pthreadStartRoutine_cf47bd191b082a3631657d369b9255c828b9
a95c(IsolateEnterStub.java:0)
"main" #1 thread=0x000055f07ff72340 state=TIMED_WAITING
SP 0x00007ffe9c0829b0 IP 0x000055f07d5ceba9 [image code]
com.oracle.svm.core.posix.headers.Pthread.pthread_cond_timedwait(Pthread.java)
SP 0x00007ffe9c0829b0 IP 0x000055f07d5ceba9 [image code]
com.oracle.svm.core.posix.thread.PosixParkEvent.condTimedWait(PosixPlatformThreads.java:346)
SP 0x00007ffe9c082a30 IP 0x000055f07d5e2232 [image code]
com.oracle.svm.core.thread.PlatformThreads.sleep0(PlatformThreads.java:943)
SP 0x00007ffe9c082a70 IP 0x000055f07d5e1ddc [image code]
com.oracle.svm.core.thread.PlatformThreads.sleep(PlatformThreads.java:910)
SP 0x00007ffe9c082aa0 IP 0x000055f07d53d02a [image code]
com.oracle.svm.core.thread.JavaThreads.sleep(JavaThreads.java:340)
SP 0x00007ffe9c082aa0 IP 0x000055f07d53d02a [image code] java.lang.Thread.sleep(Thread.java:541)
SP 0x00007ffe9c082aa0 IP 0x000055f07d53d02a [image code] Sleep.main(Sleep.java:3)
SP 0x00007ffe9c082aa0 IP 0x000055f07d53d02a [image code]
com.oracle.svm.core.JavaMainWrapper.runCore0(JavaMainWrapper.java:180)
SP 0x00007ffe9c082ad0 IP 0x000055f07d53ccd5 [image code]
com.oracle.svm.core.JavaMainWrapper.runCore(JavaMainWrapper.java:137)
SP 0x00007ffe9c082ad0 IP 0x000055f07d53ccd5 [image code]
com.oracle.svm.core.JavaMainWrapper.doRun(JavaMainWrapper.java:237)
SP 0x00007ffe9c082b00 IP 0x000055f07d55e72b [image code]
com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:223)
SP 0x00007ffe9c082b00 IP 0x000055f07d55e72b [image code]
com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_3148eece06270530b6e0d4d60311411342c82698(IsolateEnterStub
.java:0)
GC ログと同様に、スレッドダンプの内容も独自の形式です。そのため既存のスレッドダンプ解析ツールでは対応していないことに注意して下さい。
HotSpot VM のそれと大きく異なる点は Substrate VM コード(com.oracle.svm.*
)も含まれることです。HotSpot VM のスレッドダンプは通常 Java レイヤしか表示されず JVM 実装コード(C++)はほぼ出力されませんが、Substrate VM はそれ自体が Java で実装されているため JVM 実装コードもスレッドダンプにも含まれます。
ヒープダンプ
ヒープダンプを取得したい場合も Native Image 機能によるビルド時から設定が必要です。ヒープダンプ出力機能もデフォルトで Native Image に含まれません。そのため native-image
のビルドオプション --enable-monitoring=heapdump
を指定してビルドする必要があります。モニタ機能を全て有効化する all
サブオプションの指定でも可能です。
$ javac MyApp.java
$ native-image --enable-monitoring=heapdump MyApp
このビルドオプションを指定してビルドした Native Image はヒープダンプ出力機能を持ちます。ヒープダンプを出力するには実行中のアプリケーションに対して USR1
シグナルを送信して下さい。USR1
シグナルを受信した Native Image はヒープダンプをカレントディレクトリにファイル出力します。
$ kill -USR1 <native_image_pid>
Native Image は USR1
シグナルを受信すると特定のファイル名規則(svm-heapdump-<PID>-<yyyyMMddHHmmss>Z.hprof
)に従ったヒープダンプファイルをカレントディレクトリに出力します。
$ ls
svm-heapdump-1875565-20230711T072608Z.hprof
さらに GraalVM for JDK 21 からは以下の2つの JVM オプションが追加されています。なおこれらは 21 以前のバージョンでは使用できません。
JVM オプション名 | 内容 |
---|---|
-XX:HeapDumpPath= | ヒープダンプ出力パスを指定 |
-XX:+HeapDumpOnOutOfMemoryError | OutOfMemoryError 発生時にヒープダンプを出力 |
例として -XX:+HeapDumpOnOutOfMemoryError
を指定して OutOfMemoryError
を実行すると OutOfMemoryError 発生時に以下の様なメッセージとヒープダンプファイルが出力されます。
$ ./outofmemoryerror -Xmx100m -Xms100m -Xmn30m -XX:+HeapDumpOnOutOfMemoryError
Dumping heap to svm-heapdump-2977021-OOME.hprof ...
Heap dump file created [102395861 bytes in 0.122 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Array allocation too large.
...省略
$ ls
svm-heapdump-2977021-OOME.hprof
ヒープダンプファイルは既存の解析ツール(Eclipse Memory Analyzer や VisualVM)に対応しています。
HotSpot VM のそれと大きく異なる点はスレッドダンプ同様に Substrate VM コード(com.oracle.svm.*
)も含まれることです。さらに Native Image に含まれる固有の byte
情報もわずかに含まれます。
フライトレコード
フライトレコードを取得したい場合も Native Image 機能によるビルド時から設定が必要です。JDK Flight Recorder(JFR) 機能もデフォルトで Native Image に含まれません。そのため native-image
のビルドオプション --enable-monitoring=jfr
を指定してビルドする必要があります。モニタ機能を全て有効化する all
サブオプションの指定でも可能です。
$ javac MyApp.java
$ native-image --enable-monitoring=jfr MyApp
このビルドオプションを指定してビルドした Native Image は JFR 機能を持ちます。フライトレコードの記録開始をするには JVM 実行オプション –XX:StartFlightRecording=
を指定して下さい。指定可能なサブオプションは HotSpot VM のそれと同じです。
$ <native_image> –XX:StartFlightRecording=settings=default,filename=<filename>…
–XX:StartFlightRecording=
を指定して Native Image を実行することで起動時からフライトレコードの記録を開始します。Native Image を終了するとカレントディレクトリにフライトレコードがファイル出力されます。なお Native Image は jcmd
に対応していませんので、現在のところアプリケーションの外側から実行中のアプリケーションの JFR 機能に働きかけて記録を開始・停止・ダンプをできません。
フライトレコードファイルは既存の解析ツール(JDK 付属の jfr
や VisualVM、JDK Mission Control など)に対応しています。
HotSpot VM のそれと異なる点は未実装の JFR イベントがあることです。JFR 機能はまだ開発が続けられていますが多くの JFR イベントが実装されていません。各 JFR イベントの実装状況を確認する場合は GraalVM イシュー #5410 を参照して下さい。
(参考)VisualVM から見た未実装 JFR イベント
例として GraalVM for JDK 21 から取得したフライトレコードを VisualVM で読み込んだ場合は以下の項目の情報を参照できません。
- スレッドダンプ
- CPU 使用量
- Metaspace 使用量
- File IO
- Socket IO
- Error/Exception
- GC 関連の設定値
- メモリサンプラ
- ネットワーク使用量
- CPU 固有情報
- ネットワークインタフェース固有情報
- システムプロセス一覧
これは以下の JFR イベントが実装されていないためです。
JFR イベント | VisualVM メニュー | 備考 |
---|---|---|
jdk.ThreadDump | Overview - Thread dump | |
jdk.CPULoad | Monitor - CPU usage/Environment - CPU utilization | |
jdk.MetaspaceSummary | Monitor - Metaspace usage | |
jdk.FileRead | File IO | |
jdk.FileWrite | File IO | |
jdk.SocketRead | Socket IO | |
jdk.SocketWrite | Socket IO | |
jdk.JavaErrorThrow | Exceptions | |
jdk.JavaExceptionThrow | Exceptions | |
jdk.GCConfiguration | GC - GC configuration | |
jdk.GCHeapConfiguration | GC - Heap configuration | |
jdk.YoungGenerationConfiguration | GC - Young Generation configuration | |
jdk.GCSurvivorConfiguration | GC - Survivor configuration | |
jdk.GCTLABConfiguration | GC - TLAB configuration | |
jdk.ObjectCount | Sampler - Heap histogram | #7402 で実装リクエスト中 |
jdk.ThreadAllocationStatistics | Sampler - Per Thread Allocation | #7263 で JDK 22 にマージ |
jdk.CPUInformation | Environment - CPU details | |
jdk.NetworkUtilization | Environment - Network utilization | |
jdk.SystemProcess | Environment - System processes |
エラーレポート
HotSpot VM は JVM レイヤで異常(Segmentation Fault など)が発生した場合にエラーレポートをファイル出力(hs_err_pid<pid>.log
)します。Native Image の Substrate VM でも同様に異常時はエラーレポートを標準エラー出力します。
エラーレポートの出力機能は Native Image の Segfault ハンドラ実装(SubstrateSegfaultHandler
)が行いますがデフォルトで有効化されているため native-image
のビルドオプション設定は必要ありません。
以下は実行中の Native Image に対し意図的に SEGV
シグナルを送信したときのエラーレポート抜粋です。エラーレポートも Substrate VM 独自の内容が出力されるため HotSpot VM のそれとは違うことに注意して下さい。
[ [ SubstrateSegfaultHandler caught a segfault in thread 0x000055e0aad45700 ] ]
siginfo: si_signo: 11, si_code: 0, si_addr: 0x000003e800162976
Current timestamp: 1688534135235
General purpose register values:
RAX 0xfffffffffffffffc is an unknown value
RBX 0x0000000000000000
...省略
Printing Instructions (ip=0x00007ff24877a376):
0x00007ff24877a356: 0x38 0x00 0x00 0xe8 0xe2 0x3c 0x00 0x00 0x89 0xde 0x45 0x31 0xd2 0x31 0xd2 0x41
...省略
Top of stack (sp=0x00007ffcf1a60c50):
0x00007ffcf1a60c50: 0x0000000000000001 0x0000008145e3a1a8
...省略
Top frame info:
Does not look like a Java Frame. Use JavaFrameAnchors to find LastJavaSP:
Found matching Anchor:0x00007ffcf1a60d80
LastJavaSP 0x00007ffcf1a60d70
LastJavaIP 0x000055e09f439f96
Threads:
0x000055e0aad45700 STATUS_IN_JAVA (PREVENT_VM_FROM_REACHING_SAFEPOINT) "DestroyJavaVM" - 0x00007ff240c01028,
stack(0x00007ffcf1264000,0x00007ffcf1a61000)
0x00007ff1e0000b80 STATUS_IN_NATIVE (ALLOW_SAFEPOINT) "http-nio-8080-Acceptor" - 0x00007ff21200b158, daemon,
stack(0x00007ff209ffe000,0x00007ff20a7fd000)
0x00007ff1dc000b80 STATUS_IN_NATIVE (ALLOW_SAFEPOINT) "http-nio-8080-Poller" - 0x00007ff212008bc0, daemon,
stack(0x00007ff20a7ff000,0x00007ff20affe000)
0x00007ff1e8000b80 STATUS_IN_NATIVE (ALLOW_SAFEPOINT) "http-nio-8080-exec-9" - 0x00007ff212004698, daemon,
stack(0x00007ff20b801000,0x00007ff20c000000)
...省略
VM thread locals for the failing thread 0x000055e0aad45700:
0 (8 bytes): JNIThreadLocalEnvironment.jniFunctions = (bytes)
0x000055e0aad45700: 0x0000000000000000
8 (8 bytes): StackOverflowCheckImpl.stackBoundaryTL = (Word) 1 (0x0000000000000001)
16 (4 bytes): Safepoint.safepointRequested = (int) 2147458250 (0x7fff9cca)
20 (4 bytes): StatusSupport.statusTL = (int) 1 (0x00000001)
24 (32 bytes): ThreadLocalAllocation.regularTLAB = (bytes)
0x000055e0aad45718: 0x00007ff240c00000 0x00007ff240d00000
0x000055e0aad45728: 0x00007ff240c01138 0x0000000000000000
56 (8 bytes): PlatformThreads.currentThread = (Object) java.lang.Thread (0x00007ff240c01028)
...省略
No VMOperation in progress
The 15 most recent VM operation status changes (oldest first):
1688534107583 - Started Garbage collection (safepoint: true, queueingThread: 0x000055e0aad44340, executingThread:
0x000055e0aad44340, safepointId: 1)
1688534107600 - Finished Garbage collection (safepoint: true, queueingThread: 0x000055e0aad44340, executingThread:
0x000055e0aad44340, safepointId: 1)
...省略
Counters:
Java frame anchors for the failing thread 0x000055e0aad45700:
Anchor 0x00007ffcf1a60d80 LastJavaSP 0x00007ffcf1a60d70 LastJavaIP 0x000055e09f439f96
Stacktrace for the failing thread 0x000055e0aad45700:
SP 0x00007ffcf1a60c50 IP 0x00007ff24877a376 IP is not within Java code. Trying frame anchor of last Java frame
instead.
SP 0x00007ffcf1a60d70 IP 0x000055e09f439f96 [image code]
com.oracle.svm.core.thread.PlatformThreads.joinAllNonDaemonsTransition(PlatformThreads.java:354)
SP 0x00007ffcf1a60da0 IP 0x000055e09f276b08 [image code]
com.oracle.svm.core.thread.PlatformThreads.joinAllNonDaemons(PlatformThreads.java:338)
SP 0x00007ffcf1a60da0 IP 0x000055e09f276b08 [image code]
com.oracle.svm.core.JavaMainWrapper.runShutdown0(JavaMainWrapper.java:205)
SP 0x00007ffcf1a60dc0 IP 0x000055e09f276031 [image code]
com.oracle.svm.core.JavaMainWrapper.runShutdown(JavaMainWrapper.java:198)
SP 0x00007ffcf1a60dc0 IP 0x000055e09f276031 [image code]
com.oracle.svm.core.JavaMainWrapper.doRun(JavaMainWrapper.java:242)
SP 0x00007ffcf1a60df0 IP 0x000055e09f31c32b [image code]
com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:223)
SP 0x00007ffcf1a60df0 IP 0x000055e09f31c32b [image code]
com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_e6899342f5939c89e6e2f78e2c71f5f4926b786d(IsolateEnterStub
.java:0)
VM mutexes:
mutex "thread" is unlocked.
mutex "mainVMOperationControlWorkQueue" is unlocked.
mutex "jfrSymbolRepository" is unlocked.
mutex "jfrThreadRepository" is unlocked.
mutex "perfDataInitialization" is unlocked.
mutex "SamplerBufferPool" is unlocked.
mutex "jfrRecorder" is unlocked.
mutex "referencePendingList" is unlocked.
AOT compiled code is mapped at 0x000055e09eb93000 - 0x000055e0a3ce7bff
Heap settings and statistics:
Supports isolates: true
Heap base: 0x00007ff243200000
Object reference size: 8
Aligned chunk size: 1048576
Incremental collections: 3
Complete collections: 2
Native image heap boundaries:
ReadOnly Primitives: 0x00007ff243301028 - 0x00007ff244351d50
ReadOnly References: 0x00007ff244351d50 - 0x00007ff244ccb628
ReadOnly Relocatables: 0x00007ff244ccc000 - 0x00007ff2454c5668
Writable Primitives: 0x00007ff2454c6000 - 0x00007ff245644dd8
Writable References: 0x00007ff245644dd8 - 0x00007ff2462f4138
Writable Huge: 0x00007ff246300030 - 0x00007ff24660ba70
ReadOnly Huge: 0x00007ff24660c030 - 0x00007ff24850a7e8
Heap:
Young generation:
...省略
Old generation:
...省略
Segfault detected, aborting process. Use runtime option -R:-InstallSegfaultHandler if you don't want to use
SubstrateSegfaultHandler.
native-image のビルドオプション –R:-InstallSegfaultHandler を指定してビルドするか、JVM オプション -XX:-InstallSegfaultHandler を指定して実行すると Segfault ハンドラを無効化できます。無効化した場合は異常発生時にエラーレポートが出力されなくなりますが、代わりにコアダンプが出力されます。異常発生時のコアダンプ出力を期待する場合は Segfault ハンドラを無効化する必要があることに注意して下さい。
コアダンプ
ざっくりいうと Native Image は Java コード + JVM コードが等価な C++ コードに変換されて1つの実行ファイルとしてコンパイルされたものです。そのため Native Image は GDB を使用してデバッグします。
コアダンプを使用せず直接 Native Image のデバッグをする場合は GDB の代わりに VS Code プラグインを使用できます。詳細は GraalVM ドキュメントの Debug Native Images in VS Code を参照して下さい。
GDB を使用すると実行中の Native Image や Native Image から生成されたコアダンプをデバッグできます。ここでは Native Image の異常でコアダンプが生成された場合の解析の取り掛かりとして GDB でコアダンプを参照できるまでの手順をまとめます。
異常発生時にコアダンプを出力させる場合は Segfault ハンドラを無効化して下さい。
GDB の使用方法や GDB を使用した Native Image の情報を参照する手順は大変複雑なためこの記事では省略します。詳細は GraalVM ドキュメントの Debug Native Executables with GDB を参照して下さい。
コアダンプは OS の機能によるプロセスのメモリダンプですので GraalVM や Native Image 機能の設定によって取得が制限されることはありません。ただしコアダンプのデバッグにはデバッグ情報(debuginfo
)が必要です。これがない場合は GDB で Native Image のコアダンプを参照しても関数名や変数名を読み取れず変数値の参照などもできません。そのため Java コンパイルと Native Image のビルド時にデバッグ情報を生成するオプションを指定します。
デバッグ情報を生成するには javac
実行時に -g
オプションを、native-image
実行時に -g
オプションを指定します。さらに詳細なデバッグを実現するため native-image
に -O0
(オーゼロ)オプションも指定することが推奨されます。このオプションは最適化レベルを 0
(最適化なし)に指定しますが、最適化をしないことでインライン化されたコードなども参照できる様になります。以下はデバッグ情報を生成してビルドする例です。
$ javac –g MyApp.java
$ native-image –g –O0 MyApp
native-image
に -g
を指定することでデバッグ情報のファイル一式が生成されます。以下は native-image
実行ログの抜粋です。Native Image の app
だけでなく、そのデバッグ情報ファイル app.debug
とソースキャッシュ一式を含むディレクトリ sources
が生成されています。
Produced artifacts:
/path/to/build/home/app (executable)
/path/to/build/home/app.debug (debug_info)
/path/to/build/home/sources (debug_info)
GDB による Native Image のコアダンプ解析は上記の過程で得た以下のファイル一式が必要です。
- Native Image(実行ファイル)
- コアダンプ
- デバッグ情報
- ソースキャッシュ
GDB で Native Image のコアダンプを参照するには以下のコマンドを実行します。デバッグ情報ファイルとソースキャッシュは実行ディレクトリに配置していれば GDB が自動で読み込みます。
$ gdb <native_image> <coredump>
以下は GDB でコアダンプを参照し、特定のスレッドのバックトレース(スタックトレースの様なもの)を参照した2つの例です。これらはいずれも同じコアダンプを参照していますが、前者はデバッグ情報(およびソースキャッシュ)なしの場合で、後者はありの場合です。
(gdb) bt
#0 0x00007fd01dc937d1 in
pthread_cond_timedwait@@GLIBC_2.3.2 () from
/lib/x86_64-linux-gnu/libpthread.so.0
#1 0x00005612786a5a45 in ?? ()
...省略
(gdb) bt
#0 0x00007fd01dc937d1 in pthread_cond_timedwait@@GLIBC_2.3.2 () from
/lib/x86_64-linux-gnu/libpthread.so.0
#1 0x00005612786a5a45 in com.oracle.svm.core.posix.headers.Pthread::pthread_cond_timedwait(com.ora
cle.svm.core.posix.headers.Pthread$pthread_cond_t, com.oracle.svm.core.posix.headers.Pthread$pthread_mutex_t, com.oracle.svm.core.posix.headers.Time$timespec) (__0=<optimized out>, __1=<optimized out>, __2=<optimized out>)
… 省略
フレーム #1
は Substrate VM 実装部分ですが、デバッグ情報なしのため Native Image 部分の関数は ???
の表記となってしまっています。一方デバッグ情報があると実行している関数名や引数まできちんと確認できます。この様に GDB によるデバッグはデバッグ情報がほぼ必須で、デバッグ情報がないとデバッグは非常に困難になります。なおデバッグ情報を生成することで性能には影響はなくファイルサイズが増えるだけです。Native Image を使用する場合はセンシティブなファイルサイズの縛りがない限りデバッグ情報を生成しておくことを推奨します。
(参考)GDB による Native Image のデバッグで利用可能な機能
GDB を使用した Native Image のデバッグでは以下のことができます。
- Java ソースコードの表示
- ブレークポイント設定
- ステップ実行
- バックトレース表示
- プリミティブ値の表示
- Java オブジェクト(構造体表現)の表示
- Java オブジェクトのキャストと表示
- パス式を使用したオブジェクトへのアクセス
- メソッドおよび静的フィールドデータの名前による参照
- パラメータおよびローカル変数にバインドされた値の名前による参照
以下は簡単な例として Sleep
のコアダンプから Thread.sleep()
に指定した秒数のプリミティブ値を確認する例です。
(gdb) frame 8
#8 java.lang.Thread::sleep(long) (millis=10000000) at java/lang/Thread.java:509
509 sleep0(nanos);
(gdb) list ★ Thread.sleep() の Java ソース(.java)を表示
504 ThreadSleepEvent event = beforeSleep(nanos);
505 try {
506 if (currentThread() instanceof VirtualThread vthread) {
507 vthread.sleepNanos(nanos);
508 } else {
509 sleep0(nanos);
510 }
511 } finally {
512 afterSleep(event);
513 }
(gdb) print nanos
$2 = 10000000000000 ★プリミティブ値の表示
はじめに frame
コマンドでバックトレースから java.lang.Thread.sleep(long)
を実行しているフレーム #8
を指定します。ここでは 509 行目の sleep0(nanos)
を実行していることが分かります。続いて list
コマンドで Thread.sleep()
のソースキャッシュを参照しています。実行している処理は 509 行目の sleep0(nanos)
ですので、print
コマンドを実行してこれに渡されるプリミティブ値 nanos
を参照すると 10000000000000
であることが分かります。この様に GDB を使用して Java コードを参照しつつ、確認したいプリミティブ値を参照できます。
実際のところ Native Image の GDB によるデバッグはこの様な簡単なものだけではありません。先ほど Native Image は Java コードを等価な C++ に変換しているとざっくり述べましたが、実際はかなり複雑に変換しているため高度な理解と考慮が必要です。例えば Java クラスは C++ の構造体として表し、参照はポインタで示されます。つまり Java の大半の参照型オブジェクトはポインタを辿って内容を確認していく必要があり、加えて C++ や GDB に対して深い造詣が要求されます。ここでは詳しく述べませんので詳細は GraalVM ドキュメントの Debug Native Executables with GDB を参照して下さい。
まとめ
この記事では GraalVM Native Image から JVM 関連情報を取得する方法とその情報を参照する方法を記載してきました。2023年12月現在最新の GraalVM for JDK 21 では一部の JFR イベントが開発中であることを除き、おおむね JVM 関連情報が取得できる様になってきました。
最後に簡単なまとめを記載します。なお本記事の内容は GraalVM for JDK 21 Community に基づいた調査結果であり、将来的なバージョンでは取得方法や各種オプションが変更になる可能性があることに注意して下さい。
情報種別 | 取得可否 | ビルドオプション | 取得方法 | 出力形式 | 解析ツール |
---|---|---|---|---|---|
バージョン | o | - | $ strings NATIVE-IMAGE | grep “com.oracle.svm.core.VM | stdout | - |
JVM オプション | o | - | $ ps aux | grep NATIVE-IMAGE-NAME | stdout | - |
GC ログ(Serial GC) | o | - | $ NATIVE-IMAGE -XX:+PrintGC -XX:+VerboseGC ... | stderr | - |
スレッドダンプ | o | -H:+DumpThreadStacksOnSignal | $ kill -QUIT PID | stderr | - |
ヒープダンプ | o | --enable-monitoring=heapdump | $ kill -USR1 PID or $ NATIVE-IMAGE -XX:+HeapDumpOnOutOfMemoryError |
file (.hprof) |
Eclipse Memory Analyzer VisualVM |
フライトレコード | △ | --enable-monitoring=jfr | $ NATIVE-IMAGE -XX:StartFlightRecording= | file (.jfr) |
jfr VisualVM JDK Mission Control |
エラーレポート | o | - | 異常時に自動出力 | stderr | - |
コアダンプ | o | -g | OS による出力 | file(core) | GDB |
※記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。