JVM を読む | JVM の構造その7 - オブジェクトの作成とフローの制御について
前回の続きです。前回はこちらから。
このシリーズは,JVM の仕様書を読み解くためのガイドとして構成しています。
JVM の仕様書は非常に長大で難解な内容が多いため,各セクションごとに要点をまとめていきます。
また,JVM の内部構造や動作原理を知ることで,Java のパフォーマンスやセキュリティ,メモリ管理の仕組みを深く理解する試みです。
シリーズはこちらから。
第二章 The Structure of the Java Virtual Machine
JVM の仕様書の第2章は「Java Virtual Machine の構造」です。
といいましてもこの章は全7章ある JVM の仕様書の中でも特に長く,また特に複雑な内容ですので,全8回に分けて解説していきます。
ここでは Chapter 2.11.5 ~ Chapter 2.11.10 の内容を扱います。
› 2.11.5 Object Creation and Manipulation)
2.11.5 オブジェクトの作成と操作(クラス・インスタンスと配列はどちらもオブジェクト(構造を持つデータ)として扱われますが,これらの作成と操作には異なる命令を使用します。
オブジェクトの作成
オブジェクトの作成は new
系命令を使用して行います。
-
new
:クラス・インスタンスを作成します(new Object()
に相当)。 -
newarray
:プリミティブ型の配列(int[]
やbyte[]
など)を作成します。 -
anewarray
:参照型の配列(String[]
など)を作成します。 -
multianewarray
:多次元配列(int[][]
やString[][]
など)を作成します。
オブジェクトのアクセス
オブジェクトのフィールドや静的フィールドへのアクセスは,get
系命令と put
系命令を使用して行います。
-
getfield
:インスタンス・フィールドを取得します。 -
putfield
:インスタンス・フィールドに値を設定します。 -
getstatic
:静的フィールドを取得します。 -
putstatic
:静的フィールドに値を設定します。
配列のアクセス
配列の要素へのアクセスは,aload
系命令と astore
系命令を使用して行います。
前者は配列の要素を取得し,後者は配列の要素に値を設定します。
さらに,各要素の型に応じて固有の接頭辞をつけることで命令を表現します。
(byte
型の配列の場合は baload
と bastore
,char
型の配列の場合は caload
と castore
,など)
例:
aload 0 // スタックに配列をプッシュ
iconst_0 // スタックに取得したい配列要素のインデックスをプッシュ
baload // スタックのトップの配列から要素を取得し,スタックにプッシュ
配列の長さの取得
配列の長さを取得するには arraylength
命令を使用します。
この命令は,配列の長さをスタックにプッシュします。
なおこの命令は直交的であり,配列の要素の型に依存しません。
aload 0 // スタックに配列をプッシュ
arraylength // スタックのトップの配列の長さを取得し,スタックにプッシュ
› 2.11.6 Operand Stack Management Instructions)
2.11.6 オペランド・スタックを管理する命令(オペランド・スタックは,JVM の命令が操作する値を一時的に格納するためのスタックです。
以下の直交的な命令を使用して,オペランド・スタックを管理します。
-
dup
:スタックのトップの値を複製します。..., value -> ..., value, value
-
dup_x1
:スタックのトップの値を複製し,その複製をスタックの2番目に配置します。..., value1, value2 -> ..., value2, value1, value2
-
dup_x2
:スタックのトップの値を複製し,その複製をスタックの3番目に配置します。..., value1, value2, value3 -> ..., value2, value3, value1, value2
-
dup2
:スタックのトップの2つの値を複製します。..., value1, value2 -> ..., value1, value2, value1, value2
-
dup2_x1
:スタックのトップの2つの値を複製し,その複製をスタックの3番目と4番目に配置します。..., value1, value2, value3 -> ..., value2, value3, value1, value2
-
dup2_x2
:スタックのトップの2つの値を複製し,その複製をスタックの4番目と5番目に配置します。..., value1, value2, value3, value4 -> ..., value2, value3, value4, value1, value2
-
pop
:スタックのトップの値を削除します。..., value1, value2 -> ..., value1
-
pop2
:スタックのトップの2つの値を削除します。..., value1, value2, value3 -> ..., value1
-
swap
:スタックのトップの2つの値を入れ替えます。..., value1, value2 -> ..., value2, value1
› 2.11.7 Control Transfer Instructions)
2.11.7 制御の移譲命令(制御移譲命令は(条件付きや無条件で)プログラムの実行フローを変更するための命令です。
この命令は,これ以降に続く命令の実行をせずに指定された別の命令に制御を移して続行します。
以下のような命令があります:
-
goto
:無条件に指定されたラベルにジャンプします。 -
if
系命令:条件付きで指定されたラベルにジャンプします -
tableswitch
,lookupswitch
:整数値に基づいて指定されたラベルにジャンプします。 -
jsr
:サブルーチンにジャンプします。 -
ret
:サブルーチンから戻ります。
条件分岐
JVM には,int
型や参照型のデータを比較し,その上で条件に応じてジャンプするための命令が豊富に用意されています。
さらに null
参照を得た場合にジャンプするといった命令もあります。
数値型の比較では,その型に応じて異なる命令を使用します。
boolean
や byte
,char
,short
,int
型の値を比較する場合は int
型の条件分岐命令を使用します。
例:
instructions:
iconst_0 // スタックに 0 をプッシュ
iconst_0 // スタックに 0 をプッシュ
if_icmpeq label // スタックのトップの2つの値が等しい場合に label にジャンプ
// 条件が満たされなかった場合の処理 …
label:
// 条件が満たされた場合の処理 …
一方で, long
型や float
型,double
型の値を比較する場合は,最初に専用の命令でデータを比較した後に(その結果は int
型の値が帰るので),int
型の条件分岐命令を使用します。
比較命令の後に,int
型の条件分岐命令を使用することで,比較結果を検証してジャンプを行うのです。
このような使用法もあるため, JVM では int
型用の条件分岐命令が豊富に用意されています。
例:
instructions:
lconst_0 // スタックに long 型の 0 をプッシュ
lconst_0 // スタックに long 型の 0 をプッシュ
// スタックのトップの2つの long 型の値を比較し,結果を int 型でスタックにプッシュ
// 比較結果は 0(等しい)か,1(左辺が大きい)か,-1(右辺が大きい)のいずれかです。
lcmp
iconst_0 // スタックに 0 をプッシュ
// スタックのトップの2つの値が等しい場合に label にジャンプする。
// この場合は,0 と比較しているので,2つの long 型の値が等しい場合にジャンプします。
if_icmpeq label
// 条件が満たされなかった場合の処理 …
label:
// 条件が満たされた場合の処理 …
› 2.11.8 Method Invocation and Return Instructions)
2.11.8 メソッドの呼び出しとリターン命令(メソッドの呼び出しとリターン(return)は,JVM の重要な機能の一つです。
メソッドの呼び出しは,invoke
系命令を使用して行い,メソッドから戻るときは return
系命令を使用します。
メソッドの呼び出し
メソッドの呼び出しは,呼び出すメソッドがどのような種類のものであるかによって異なる命令を使用します。
具体的には以下のような命令があります:
-
invokestatic
:静的メソッドを呼び出します。 -
invokevirtual
:インスタンスメソッドを呼び出します。 -
invokespecial
:特別なメソッド)(コンストラクタやスーパークラスのメソッドなど)を呼び出します。 -
invokeinterface
:インタフェースメソッドを呼び出します。 -
invokedynamic
:動的にメソッドを解決して呼び出します。
invokedynamic
命令は,Java 7 以降で導入された機能であり,動的なメソッド呼び出しをサポートします。
この命令の最大の特徴かつ難解な部分は,呼び出し対象のメソッドを具体的に決定するのが実行時(より正確には,リンク時)であることです。
この動的解決は,invokedynamic
命令が呼び出されるときに,ブートストラップ・メソッド(bootstrap method) と呼ばれる特別なメソッドが呼び出されることで行われます。
なお,ブートストラップ・メソッドは invokedynamic
のオペランドとして,呼び出すメソッドの名前およびシグニチャとともに指定されます。
ブートストラップ・メソッドは通常 ACC_STATIC
フラグを持つ静的メソッドであり,以下のようなシグニチャを持ちます:
static CallSite bootstrap(
MethodHandles.Lookup caller,
String name,
MethodType type,
Object... args
);
なお,メソッドの引数は自由に指定できますが,MethodHandles.Lookup
型の引数を必ず最初に持ちます。
このメソッドは invokedynamic
のリンク時に一度だけ呼ばれ,適切なメソッドハンドルを含む CallSite
を返します。
リンクされた CallSite
は,そのクラス・メソッド・命令に対してキャッシュされ,次回以降の同じ命令の呼び出しでは直接使用されます。
ぺやんぐ注
invokedynamic
命令は,Java 言語のラムダ式やメソッド参照などの機能を実現するために使用されます。
例えば以下のようなラムダ式を考えます:
Function<Integer, Integer> square = x -> x * x;
このラムダ式は invokedynamic
命令を使用して実行時に解決されます。
具体的には,invokedynamic
命令が呼び出されると,java/lang/invoke/LambdaMetafactory
クラスのブートストラップ・メソッド metafactory
が呼び出されます。
JVM は以下のような流れでこのメソッドを呼び出して,ラムダ式の実行時の型を解決して適切なメソッドハンドルを返します。
-
invokedynamic
命令に差し掛かる。 - JVM は最初の呼び出しで
LambdaMetafactory->metafactory
を実行する。 - これが
CallSite
を返し,(その情報を基に)そこにラムダ式の本体へのMethodHandle
が登録される。 - 以降の呼び出しでは,そのハンドルを直接使って高速に呼び出す。
このように,invokedynamic
命令は,Java 言語の静的型と動的動作を両立するための柔軟かつ高性能な基盤として機能しています。
従来の JVM 呼び出し命令が持たなかった動的な振る舞いを持つ唯一の命令であり,Java の進化に欠かせない存在となっています。
メソッドのリターン
メソッドのリターン命令は,戻り値の型によって次のように区別されます。
-
ireturn
:int
型の値(boolean
やbyte
,char
,short
も含む)を返します。 -
lreturn
:long
型の値を返します。 -
freturn
:float
型の値を返します。 -
dreturn
:double
型の値を返します。 -
areturn
:参照型の値を返します。 -
return
:戻り値のないメソッド(void
型)から戻ります。
› 2.11.9 Throwing Exceptions)
2.11.9 例外のスロー(例外のスローは,athrow
命令を使用して行います。
この命令は,スタックのトップにある例外オブジェクトをスローします。
また, JVM が各種命令の実行時に異常な状態に遭遇した場合は,例外をスローします。
これらの例外は,通常のプログラムの制御フローを中断し,例外処理のための特別なフローに移行します。
› 2.11.10 Synchronization)
2.11.10 同期(JVM はスレッド間の同期制御を直にサポートしており,複数のスレッドが同じオブジェクトにアクセスする際の競合状態を防ぐための機能を提供します。
これには,JVM のモニタ(monitor) 機能が用いられます。
モニタはメソッド,或いはメソッド内の命令列に対して排他制御を提供します。
すべてのオブジェクトはモニタを持ち,モニタは所有権を持つスレッドとエントリ・カウントを追跡します。
所有権を持つスレッドは,そのモニタを獲得しているスレッドのことです。
エントリ・カウントは,そのスレッドがそのモニタを獲得した回数を表します。
モニタを獲得するには,スレッドはそのモニタの所有権を取得し,エントリ・カウントをインクリメントします。
モニタを解放するには,エントリ・カウントをデクリメントし,エントリ・カウントがゼロになった場合に所有権を解放します。
或るスレッドがモニタを取得している間は,他のスレッドがそのモニタを取得できません。
他のスレッドがモニタを取得しようとすると,そのスレッドの実行はモニタが解放されるまでブロックされます(WAITING
状態になります)。
メソッドの同期
メソッドに ACC_SYNCHRONIZED
フラグ(synchronized
フラグ)が設定されている場合,そのメソッドは同期メソッドとなります。
同期メソッドの呼び出す際に行う特別な処理は,プログラムに書き込まれることはありません。
そのため,JVM は,プログラムが同期メソッドを呼び出すときに自動的にモニタの獲得と解放を行います。
同期メソッドを呼び出すためには,そのメソッドが属するオブジェクトのモニタを獲得する必要があります。 同期メソッドの実行が(正常・異常を問わず)修了すると,そのモニタは自動的に解放されます。
前述の通り,或るスレッドがモニタを獲得している間は,他のスレッドがそのモニタを獲得できません。
そのため,複数のスレッドが同じオブジェクトの同期メソッドを同時に呼び出そうとすると, 最初にモニタを獲得したスレッドだけがメソッドを実行でき,その他のスレッドはモニタが解放されるまでブロックされます。
命令列の同期
命令列の同期は,Java における synchronized
ブロックの実装によって行われます。
JVM はこの同期を提供するために, monitorenter
と monitorexit
命令を実装しています。 これは構造化ロック(Structured-locking)を実現するための重要な命令です。
構造化ロックとは,モニタの獲得と解放が静的なコード構造に従ってペアで現れることを前提とする考え方です。
すなわち或るブロックで獲得されたモニタが,必ずそのブロックの終わり(または例外による脱出)で必ず解放されることが期待されます。
ただし,JVM が実行するすべてのコードが適切な構造化ロックを記述している保証はありません。
例えば,バイト・コードを直接生成するツールや,JVM の命令セットを直接操作するような低レベルのコードでは,意図せずに構造化ロックが守られない場合があります。
そのため JVM は構造化ロックを確実に履行するために,以下の2つのルールを強制することがあります:
- 或るスレッドが或るモニタを獲得する回数は,そのスレッドがそのモニタを解放する回数と同じであること
→ 取得と解放がペアであることを保証します。 - メソッド呼び出しのどの時点でも,或るスレッドが或るモニタに対して行った開放の回数が,獲得の回数を上回らないこと
→ 解放が獲得を上回らないことを保証します。
まとめ
この記事では JVM の仕様書の第2章の一部を解説しました。
JVM の構造やオブジェクトの作成と操作,制御の移譲命令,メソッドの呼び出しとリターン命令,例外のスロー,同期などについて詳しく説明しました。
特に制御の移譲命令では,条件分岐やメソッドの呼び出しとリターン命令について詳しく解説しました。
整数型の条件分岐命令では一部の型に対しては専用の命令が用意されていることや,invokedynamic
命令の動的なメソッド呼び出しについても触れました。
また,例外のスローや同期の仕組みについても解説しました。
これらの知識は JVM の内部構造や動作原理を理解するために重要です。
次回は閑話として JVM が満たすべき要件と原則(Chapter 2.12, Chapter 2.13)について解説します。
では,よいバイト・コードライフを!
次回リンク
参考文献&リンク集
- Lindholm, T., Yellin, F., Bracha, G., & Smith, W. M. D. (2025). The Java® Virtual Machine Specification: Java SE 24 Edition.
- Lindholm, T., & Yellin, F. (1999). The Java™ Virtual Machine Specification (2nd ed.). Addison-Wesley. ISBN 978-0-201-43294-7
- Otavio, S. (2024). Mastering the Java Virtual Machine. Packet Publishing. ISBN 978-1-835-46796-1
Discussion