はじめに
この記事では、Semeru Runtime/OpenJ9のJITコンパイラーの動作を制御するためのコマンドラインオプションについて説明します。
JITコンパイラーについては以前の記事も参照してください。
-Xjit
基本は -Xjit オプションです。このオプションは指定しなくてもデフォルトで有効ですが、様々なサブオプションがあります。
-Xjit:verbose
最初は verbose サブオプションです。このオプションを指定すると、JIT/AOTでコンパイルされたJavaメソッド (あるいはコンパイルが失敗したJavaメソッド) の情報が標準エラー出力に出力されます。標準エラー出力では読みにくくて困る場合が多いと考えられるので、通常は vlog=filename サブオプションと組み合わせることで指定したファイル名に出力します。
$ jdk-17.0.18+8/bin/java -Xjit:verbose,vlog=hello.log HelloWorld
Hello, World!
$ less hello.log.20260329.210112.239671
#INFO: _______________________________________
#INFO: Version Information:
#INFO: JIT Level - j9jit_20260223_2135_
#INFO: JVM Level - 20260120_1216
#INFO: GC Level - f0754f4102
#INFO:
#INFO: _______________________________________
(中略)
+ (cold) java/lang/System.getSysPropBeforePropertiesInitialized(I)Ljava/lang/String; @ 0000FC774EC0002C-0000FC774EC00168 OrdinaryMethod - Q_SZ=1 Q_SZI=1 QW=2 j9m=0000FC776C13EC10 bcsz=3 JNI compThreadID=0
+ (cold) java/lang/Double.longBitsToDouble(J)D @ 0000FC774EC00184-0000FC774EC001C8 OrdinaryMethod - Q_SZ=0 Q_SZI=0 QW=1 j9m=0000FC776C1474A8 bcsz=3 JNI compThreadID=0
+ (cold) jdk/internal/reflect/Reflection.getCallerClass()Ljava/lang/Class; @ 0000FC774EC001E4-0000FC774EC00314 OrdinaryMethod - Q_SZ=0 Q_SZI=0 QW=1 j9m=0000FC776C13D0C8 bcsz=2 JNI compThreadID=0
+ (cold) jdk/internal/misc/Unsafe.objectFieldOffset1(Ljava/lang/Class;Ljava/lang/String;)J @ 0000FC774EC00334-0000FC774EC0048C OrdinaryMethod - Q_SZ=0 Q_SZI=0 QW=1 j9m=0000FC776C13A238 bcsz=4 JNI compThreadID=0
+ (AOT warm) com/ibm/jit/JITHelpers.getByteFromArrayByIndex(Ljava/lang/Object;I)B @ 0000FC774EC004E0-0000FC774EC00B40 OrdinaryMethod - Q_SZ=2 Q_SZI=2 QW=13 j9m=0000FC776C137428 bcsz=100 GCR compThreadID=0
(中略)
#INFO: Stopping compilation thread, vmThread pointer 000000000001DC00, thread ID 6
! (AOT warm) java/util/HashMap.putVal(ILjava/lang/Object;Ljava/lang/Object;ZZ)Ljava/lang/Object; Q_SZ=0 Q_SZI=0 QW=12 j9m=0000FC776C14AEA8 time=4509us compilationInterrupted VmState=0x000501ff memLimit=262144 KB compThreadID=0
#INFO: Stopping compilation thread, vmThread pointer 000000000000FD00, thread ID 0
#INFO: Stopping compilation thread, vmThread pointer 0000000000020200, thread ID 7
+ で始まる行はJIT/AOTコンパイルが成功した、あるいはSCCファイルからのAOTロードが成功したJavaメソッドを示しています。
(cold) や (AOT warm) の部分は最適化レベル (後述) と、JIT/AOTコンパイルの区別を示しています。 AOT と書かれていなければJITコンパイルです。Javaメソッドのクラス名・メソッド名・シグネチャが続き、@ の後ろは生成されたネイティブコードのアドレス範囲を示しています。
一方、! で始まる行は何らかの理由でJIT/AOTコンパイルやAOTロードが失敗したことを表しています。上記の例では、HashMap.putVal() をAOTコンパイルしている途中でこのJavaプロセスが終了してしまったことが compilationInterrupted から読み取れます。
verbose サブオプションにはさらにサブオプションを与えることができます。その中の一つが compilePerformance で、JIT/AOTコンパイルにかかった時間と作業用メモリー量を出力します。
下記は -Xjit:verbose={compilePerformance} を指定した場合の出力の例です。 String.hashCode() をwarmレベルでAOTコンパイルしています。
(AOT warm) Compiling java/lang/String.hashCode()I OrdinaryMethod j9m=0000E669CC136050 t=40 compThreadID=0 memLimit=262144 KB freePhysicalMemory=7111 MB
+ (AOT warm) java/lang/String.hashCode()I @ 0000E669AFE016F0-0000E669AFE01A38 OrdinaryMethod - Q_SZ=3 Q_SZI=3 QW=30 j9m=0000E669CC136050 bcsz=74 GCR time=719us mem=[region=1984 system=16384]KB compThreadID=0 queueTime=1940us
行頭に + も ! もない Compiling の行がコンパイルを開始したタイミングで出力されています。
+ (AOT warm) の行には time= や mem= の出力が増えていて、上記の例ではこのメソッドをコンパイルするのに719マイクロ秒 (=0.719ミリ秒) かかったことが分かります。
-Xjit:optlevel
次は、コンパイルの最適化レベルを指定する optlevel サブオプションです。
OpenJ9 のJIT/AOTコンパイラーには noOpt, cold, warm, hot, veryHot, scorching の6段階の最適化レベルがあります。一般的に、最適化レベルが高いほどJIT/AOTコンパイルに時間がかかる代わりに生成されたネイティブコードの実行速度は速くなります。
optlevel を指定しない場合、Javaメソッドは初め cold か warm でコンパイルされます。その後、最適化に長い時間をかけてコンパイルする価値があると判断されたJavaメソッドは上位の最適化レベルで再コンパイルされます。Javaメソッドがどの最適化レベルでコンパイルされたかは -Xjit:verbose の出力で知ることができます。最も弱い noOpt は通常の実行では使われず、デバッグ用です。
-Xjit:optlevel=hot のように書くことで最適化レベルを明示的に指定することができます。ただし、無闇に高い最適化レベルを指定するとJavaプログラムの起動にかかる時間が長くなるのを体感できます。
-Xjit:count
JIT/AOTコンパイラーがコンパイルすべきJavaメソッドを選ぶ基準は多数ありますが、メソッドが呼ばれた回数はその中の一つです。count サブオプションはその回数を指定できます。
-Xjit:count=0 を指定すると、Javaメソッドが呼ばれた初回にそれをJITコンパイルして実行します。次項の limit を使って対象を絞らないと実行する全てのJavaメソッドをJITコンパイルすることになるので起動が遅くなります。
-Xjit:limit, exclude
limit および exclude サブオプションはJIT/AOTコンパイルの対象にするJavaメソッドを制御するために使います。メソッドの指定にはワイルドカード (*) が使用可能です。
例として -Xjit:limit={java/lang/*.a*} のように指定すると、コンパイル対象を java/lang パッケージ内のクラスの a で始まるメソッドに限定することができます。同様の書式で exclude はコンパイルの対象から除外するメソッドを指定できます。
一つの -Xjit オプションの中で limit や exclude を複数指定することも可能です。
JIT/AOTコンパイラーにバグがあって問題が発生している場合、-Xjit:verbose オプションでどのメソッドがコンパイルされたか確認し、-Xjit:limit と -Xjit:exclude を使って問題を起こしているメソッドを特定するのが典型的なデバッグの第一歩です。
上記以外にも公式ドキュメントには載っていないデバッグ用の -Xjit サブオプションが多数あります。
-Xaot
-Xaot はAOTコンパイルの動作を指定するオプションです。デフォルトでAOTは有効になっています。
-Xjit と同様のサブオプションを使えますが一部 -Xaot 固有のものがあります。loadLimit と loadExclude がその例です。書式は limit / exclude と同じです。
loadLimit は、指定されたメソッドのAOTコードがSCC内にある場合はそれをロードしますが、それ以外のメソッドのAOTコードはロードしません。必要であればAOTコードをロードする代わりにJITコンパイルします。loadExclude はその逆で、指定されたメソッドのAOTコードがSCC内にあってもそれをロードしなくなります。
-Xquickstart
-Xquickstart は適用する最適化を減らすことでJITコンパイルの時間を短縮し、その結果としてJavaアプリケーションの起動を速くします。一度起動したら何時間も実行し続けるようなサーバープログラムではなく、短時間の実行を頻繁に繰り返すようなプログラムでの使用に向いています。
-Xtune:virtualized
-Xtune:virtualized はコンテナ環境のようにCPUリソースに制約がある場合に使います。JITコンパイラーがJavaメソッドのインライニングや再コンパイルを減らすことでJITコンパイラーによるCPU時間の消費を抑制する、AOTが有効な場合はより積極的にAOTを使うなどの効果があります。CPUリソースが潤沢な環境の場合はこのオプションを指定しないほうがパフォーマンスは高くなります。
:virtualized なしの -Xtune オプションは存在しません。
-XcompilationThreads
JIT/AOTコンパイルに使用するスレッドの数を設定するオプションです。デフォルトのスレッド数は、CPUのコア数などを考慮して決められます。
OpenJ9のJIT/AOTコンパイルはJavaプログラムを実行するスレッドとは別のスレッドで行われ、2つ以上のスレッドを使用できる状態であれば複数のJavaメソッドをそれぞれ別のスレッドで並行してJIT/AOTコンパイルすることが可能です。
-Xjit:verbose オプションの出力例で行末に "compThreadID=0" とあるのがコンパイルスレッドにつけられた番号を示しています。"Stopping compilation thread" の行では "thread ID 7" まであったことが分かります。
リソースに制約がある環境で、コンパイルスレッド数を明示的に限定するために使用することがあります。
-XX:+PrintCodeCache
JITコンパイラーが生成したネイティブコードを格納するメモリー空間はcode cacheと呼ばれます。-XX:+PrintCodeCache は、Java プログラムの終了時にcode cacheについての情報を出力します。
$ jdk-17.0.18+8/bin/java -XX:+PrintCodeCache HelloWorld
Hello, World!
CodeCache: size=262144kB used=100kB max_used=100kB free=262044kB
この実行例では、code cacheの大きさは262144KB (256MB)確保されていて、HelloWorldの実行でその中の100KBを使ったことが分かります。なお、code cacheのメモリー空間は仮想記憶の仕組みを使ってアドレスを予約してあるだけで、起動時からcode cacheとしていきなり256MBのメモリーをコミットするわけではありません。
code cache関連のオプションとしては他に -Xcodecache と -Xcodecachetotal があります。
-Xcodecache はcode cahe用のメモリーを必要に応じてコミットしていく際のブロックのサイズを設定しますが、このオプションを使う場面は想定しづらいです。一方 -Xcodecachetotal はcode cacheとして使用するサイズの上限を設定します。こちらはメモリー量に制約のある環境で使う場面があるかもしれません。
-XX:+MergeCompilerOptions
コマンドラインに複数の -Xjit オプションがあった場合、最後に指定されたものだけが有効になり他は無視されるのがデフォルトの挙動です。
-XX:+MergeCompilerOptions を使うと複数の -Xjit の指定内容を一つにまとめることができます。複数の -Xaot があった場合も同様に一つにまとめられます。主にJITコンパイラーのデバッグ時に使用します。
-Xint
-Xint を指定するとJITとAOTが無効化され、バイトコードインタプリターだけを使用してJavaプログラムを実行します。
遅くてもいいから少ないメモリーフットプリントでJavaプログラムを実行したい場合や、何らかの問題が発生したときにJIT/AOTのバグか否かを確認したい場合に使用します。
参照
-
-Xjit: https://eclipse.dev/openj9/docs/xjit/ -
-Xaot: https://eclipse.dev/openj9/docs/xaot/ -
-Xquickstart: https://eclipse.dev/openj9/docs/xquickstart/ -
-Xtune:virtualized: https://eclipse.dev/openj9/docs/xtunevirtualized/ -
-XcompilationThreads: https://eclipse.dev/openj9/docs/xcompilationthreads/ -
-XX:+PrintCodeCache: https://eclipse.dev/openj9/docs/xxprintcodecache/ -
-XX:+MergeCompilerOptions: https://eclipse.dev/openj9/docs/xxmergecompileroptions/ -
-Xint: https://eclipse.dev/openj9/docs/xint/