🖥

JVM を読む | JVM の構造その7 - オブジェクトの作成とフローの制御について

に公開

前回の続きです。前回はこちらから。

https://zenn.dev/peyang/articles/reading-jvm-chapter-02-11-1-4

このシリーズは,JVM の仕様書を読み解くためのガイドとして構成しています。
JVM の仕様書は非常に長大で難解な内容が多いため,各セクションごとに要点をまとめていきます。
また,JVM の内部構造や動作原理を知ることで,Java のパフォーマンスやセキュリティ,メモリ管理の仕組みを深く理解する試みです。

シリーズはこちらから。

https://zenn.dev/peyang/articles/reading-jvm-chapter-00

第二章 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 オブジェクトの作成と操作(› 2.11.5 Object Creation and Manipulation

クラス・インスタンスと配列はどちらもオブジェクト(構造を持つデータ)として扱われますが,これらの作成と操作には異なる命令を使用します。

オブジェクトの作成

オブジェクトの作成は new 系命令を使用して行います。

  • new:クラス・インスタンスを作成します(new Object() に相当)。
  • newarray:プリミティブ型の配列(int[]byte[] など)を作成します。
  • anewarray:参照型の配列(String[] など)を作成します。
  • multianewarray:多次元配列(int[][]String[][] など)を作成します。

オブジェクトのアクセス

オブジェクトのフィールドや静的フィールドへのアクセスは,get 系命令と put 系命令を使用して行います。

  • getfield:インスタンス・フィールドを取得します。
  • putfield:インスタンス・フィールドに値を設定します。
  • getstatic:静的フィールドを取得します。
  • putstatic:静的フィールドに値を設定します。

配列のアクセス

配列の要素へのアクセスは,aload 系命令と astore 系命令を使用して行います。
前者は配列の要素を取得し,後者は配列の要素に値を設定します。

さらに,各要素の型に応じて固有の接頭辞をつけることで命令を表現します。
byte 型の配列の場合は baloadbastorechar 型の配列の場合は caloadcastore,など)

例:

aload 0     // スタックに配列をプッシュ
iconst_0    // スタックに取得したい配列要素のインデックスをプッシュ
baload      // スタックのトップの配列から要素を取得し,スタックにプッシュ

配列の長さの取得

配列の長さを取得するには arraylength 命令を使用します。
この命令は,配列の長さをスタックにプッシュします。
なおこの命令は直交的であり,配列の要素の型に依存しません。

aload 0     // スタックに配列をプッシュ
arraylength // スタックのトップの配列の長さを取得し,スタックにプッシュ

2.11.6 オペランド・スタックを管理する命令(› 2.11.6 Operand Stack Management Instructions

オペランド・スタックは,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 制御の移譲命令(› 2.11.7 Control Transfer Instructions

制御移譲命令は(条件付きや無条件で)プログラムの実行フローを変更するための命令です。
この命令は,これ以降に続く命令の実行をせずに指定された別の命令に制御を移して続行します。

以下のような命令があります:

  • goto:無条件に指定されたラベルにジャンプします。
  • if 系命令:条件付きで指定されたラベルにジャンプします
  • tableswitch, lookupswitch:整数値に基づいて指定されたラベルにジャンプします。
  • jsr:サブルーチンにジャンプします。
  • ret:サブルーチンから戻ります。

条件分岐

JVM には,int 型や参照型のデータを比較し,その上で条件に応じてジャンプするための命令が豊富に用意されています。
さらに null 参照を得た場合にジャンプするといった命令もあります。

数値型の比較では,その型に応じて異なる命令を使用します。
booleanbytecharshortint 型の値を比較する場合は 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 メソッドの呼び出しとリターン命令(› 2.11.8 Method Invocation and Return Instructions

メソッドの呼び出しとリターン(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 は以下のような流れでこのメソッドを呼び出して,ラムダ式の実行時の型を解決して適切なメソッドハンドルを返します。

  1. invokedynamic 命令に差し掛かる。
  2. JVM は最初の呼び出しで LambdaMetafactory->metafactory を実行する。
  3. これが CallSite を返し,(その情報を基に)そこにラムダ式の本体への MethodHandle が登録される。
  4. 以降の呼び出しでは,そのハンドルを直接使って高速に呼び出す。

このように,invokedynamic 命令は,Java 言語の静的型と動的動作を両立するための柔軟かつ高性能な基盤として機能しています。
従来の JVM 呼び出し命令が持たなかった動的な振る舞いを持つ唯一の命令であり,Java の進化に欠かせない存在となっています。

メソッドのリターン

メソッドのリターン命令は,戻り値の型によって次のように区別されます。

  • ireturnint 型の値(booleanbytecharshort も含む)を返します。
  • lreturnlong 型の値を返します。
  • freturnfloat 型の値を返します。
  • dreturndouble 型の値を返します。
  • areturn:参照型の値を返します。
  • return:戻り値のないメソッド(void 型)から戻ります。

2.11.9 例外のスロー(› 2.11.9 Throwing Exceptions

例外のスローは,athrow 命令を使用して行います。
この命令は,スタックのトップにある例外オブジェクトをスローします。
また, JVM が各種命令の実行時に異常な状態に遭遇した場合は,例外をスローします。

これらの例外は,通常のプログラムの制御フローを中断し,例外処理のための特別なフローに移行します。

2.11.10 同期(› 2.11.10 Synchronization

JVM はスレッド間の同期制御を直にサポートしており,複数のスレッドが同じオブジェクトにアクセスする際の競合状態を防ぐための機能を提供します。
これには,JVM のモニタ(monitor) 機能が用いられます。
モニタはメソッド,或いはメソッド内の命令列に対して排他制御を提供します。

すべてのオブジェクトはモニタを持ち,モニタは所有権を持つスレッドとエントリ・カウントを追跡します。
所有権を持つスレッドは,そのモニタを獲得しているスレッドのことです。
エントリ・カウントは,そのスレッドがそのモニタを獲得した回数を表します。

モニタを獲得するには,スレッドはそのモニタの所有権を取得し,エントリ・カウントをインクリメントします。
モニタを解放するには,エントリ・カウントをデクリメントし,エントリ・カウントがゼロになった場合に所有権を解放します。

或るスレッドがモニタを取得している間は,他のスレッドがそのモニタを取得できません。
他のスレッドがモニタを取得しようとすると,そのスレッドの実行はモニタが解放されるまでブロックされます(WAITING 状態になります)。

メソッドの同期

メソッドに ACC_SYNCHRONIZED フラグ(synchronized フラグ)が設定されている場合,そのメソッドは同期メソッドとなります。

同期メソッドの呼び出す際に行う特別な処理は,プログラムに書き込まれることはありません。
そのため,JVM は,プログラムが同期メソッドを呼び出すときに自動的にモニタの獲得と解放を行います。

同期メソッドを呼び出すためには,そのメソッドが属するオブジェクトのモニタを獲得する必要があります。 同期メソッドの実行が(正常・異常を問わず)修了すると,そのモニタは自動的に解放されます。

前述の通り,或るスレッドがモニタを獲得している間は,他のスレッドがそのモニタを獲得できません。
そのため,複数のスレッドが同じオブジェクトの同期メソッドを同時に呼び出そうとすると, 最初にモニタを獲得したスレッドだけがメソッドを実行でき,その他のスレッドはモニタが解放されるまでブロックされます。

命令列の同期

命令列の同期は,Java における synchronized ブロックの実装によって行われます。

JVM はこの同期を提供するために, monitorentermonitorexit 命令を実装しています。 これは構造化ロック(Structured-locking)を実現するための重要な命令です。

構造化ロックとは,モニタの獲得と解放が静的なコード構造に従ってペアで現れることを前提とする考え方です。
すなわち或るブロックで獲得されたモニタが,必ずそのブロックの終わり(または例外による脱出)で必ず解放されることが期待されます。

ただし,JVM が実行するすべてのコードが適切な構造化ロックを記述している保証はありません
例えば,バイト・コードを直接生成するツールや,JVM の命令セットを直接操作するような低レベルのコードでは,意図せずに構造化ロックが守られない場合があります。

そのため JVM は構造化ロックを確実に履行するために,以下の2つのルールを強制することがあります:

  1. 或るスレッドが或るモニタを獲得する回数は,そのスレッドがそのモニタを解放する回数と同じであること
    → 取得と解放がペアであることを保証します。
  2. メソッド呼び出しのどの時点でも,或るスレッドが或るモニタに対して行った開放の回数が,獲得の回数を上回らないこと
    → 解放が獲得を上回らないことを保証します。

まとめ

この記事では JVM の仕様書の第2章の一部を解説しました。
JVM の構造やオブジェクトの作成と操作,制御の移譲命令,メソッドの呼び出しとリターン命令,例外のスロー,同期などについて詳しく説明しました。
特に制御の移譲命令では,条件分岐やメソッドの呼び出しとリターン命令について詳しく解説しました。
整数型の条件分岐命令では一部の型に対しては専用の命令が用意されていることや,invokedynamic 命令の動的なメソッド呼び出しについても触れました。
また,例外のスローや同期の仕組みについても解説しました。

これらの知識は JVM の内部構造や動作原理を理解するために重要です。

次回は閑話として JVM が満たすべき要件と原則(Chapter 2.12, Chapter 2.13)について解説します。
では,よいバイト・コードライフを!

次回リンク

https://zenn.dev/peyang/articles/reading-jvm-chapter-02-12-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
GitHubで編集を提案

Discussion