JVMバイトコード命令の深い理解:後編

前回の記事ではJVMのバイトコード命令のうち、ロード・ストア命令、算術命令、型変換命令について解説しました。本記事ではバイトコード命令の後編として、メソッド呼び出しと戻り値、制御転送、例外処理、同期処理に関する命令について詳しく解説します。

IntelliJ IDEAのjclasslibプラグインを使用してコンパイル後のバイトコード命令を確認することができます。

メソッド呼び出しと戻り値命令

メソッド呼び出し命令

非仮想メソッド:静的メソッド、プライベートメソッド、親クラスのメソッド、final修飾子が付いたメソッド、インスタンスコンストラクタ

これら以外のメソッドは仮想メソッドとなります。

  • 通常の呼び出し命令

    • invokestatic: 静的メソッドを呼び出す
    • invokespecial: プライベートメソッド、親クラスのメソッド、インスタンスコンストラクタ、finalメソッドを呼び出す
    • invokeinterface: インターフェースメソッドを呼び出す
    • invokevirtual: 仮想メソッドを呼び出す

    invokestaticinvokespecial命令で呼び出されるメソッドは必ず非仮想メソッドです。

    invokeinterface命令で呼び出されるメソッドは必ず仮想メソッドです(インターフェースメソッドは具象実装クラスによって実装されるため)。

    invokevirtual命令で呼び出されるメソッドは仮想メソッドである可能性があります。

  • 動的呼び出し命令

    • invokedynamic: 呼び出すメソッドを動的に解決して実行する

    invokedynamicはJDK 7で導入され、動的言語のサポートを目的としています。

仮想メソッドのテストコード

親クラス

public class Parent {
    public static void staticMethod() {
        System.out.println("親クラスの静的メソッド");
    }

    public final void finalMethod() {
        System.out.println("親クラスのfinalメソッド");
    }

    public Parent() {
        System.out.println("親クラスのコンストラクタ");
    }

    public void overridableMethod() {
        System.out.println("親クラスのオーバーライド可能メソッド");
    }
}

インターフェース

public interface SampleInterface {
    void interfaceMethod();
}

子クラス

public class Child extends Parent {
    
    public Child() {
        // invokespecial: 親クラスのコンストラクタ呼び出し(非仮想メソッド)
        super();
        // invokestatic: 親クラスの静的メソッド呼び出し(非仮想メソッド)
        staticMethod();
        // invokespecial: 子クラスのプライベートメソッド呼び出し(特殊な非仮想メソッド)
        privateMethod();
        // invokevirtual: 子クラスのオーバーライドメソッド呼び出し(仮想メソッド)
        overridableMethod();
        // invokespecial: 親クラスのメソッド呼び出し(非仮想メソッド)
        super.overridableMethod();
        // invokespecial: 親クラスのfinalメソッド呼び出し(非仮想メソッド)
        super.finalMethod();
        // invokedynamic: インターフェースの実装クラスを動的に生成して動的呼び出し
        SampleInterface sample = () -> {
            System.out.println("インターフェースメソッドの実装");
        };
        // invokeinterface: インターフェースメソッド呼び出し(仮想メソッド)
        sample.interfaceMethod();
    }

    @Override
    public void overridableMethod() {
        System.out.println("子クラスのオーバーライドメソッド");
    }

    private void privateMethod() {
        System.out.println("子クラスのプライベートメソッド");
    }

    public static void main(String[] args) {
        new Child();
    }
}

メソッド戻り値命令

メソッド戻り値命令:メソッド終了前に、スタックの最上位要素(最後の要素)をポップし、呼び出し元に返す

メソッドの戻り値の型に応じて複数の命令が存在します。

オペランドスタック管理命令

汎用的な命令で、型を区別しません

  • ポップ命令

    • pop/pop2: スタックの最上位要素を1つ/2つポップする
  • プッシュ命令

    • dup/dup2: スタックの最上位要素を1つ/2つのスロットコピーして再びプッシュする
    • dup_x1: スタックの最上位要素を1つのスロットコピーし、スタックの2番目のスロットの下に挿入する
    • dup_x2: スタックの最上位要素を1つのスロットコピーし、スタックの3番目のスロットの下に挿入する
    • dup2_x1: スタックの最上位要素を2つのスロットコピーし、スタックの3番目のスロットの下に挿入する
    • dup2_x2: スタックの最上位要素を2つのスロットコピーし、スタックの4番目のスロットの下に挿入する

    挿入先のスロット計算: dupの係数 + _xの係数

制御転送命令

条件分岐命令

通常、まず比較命令を実行し、その後条件分岐命令を実行します

比較命令の結果(-1、0、1)に基づいて分岐するかどうかを判断します。

条件分岐命令:スタックの最上位要素をポップし、条件を満たすかどうかを判断し、満たす場合は指定された位置に分岐する

注意:これらの分岐命令は通常「否定形」で使用されます。例えば、コード内の最初の条件文がd > 100の場合、最初の条件分岐命令はifle(0以下の場合)となり、条件を満たす場合は分岐し、満たさない場合は順に実行を続けます。

比較条件分岐命令

比較条件分岐命令は、比較命令と条件分岐命令を組み合わせたものに似ています

複数条件分岐命令

複数条件分岐命令はswitch-case文のために導入されました

tableswitchはcase値が連続しているswitch文の複数条件分岐命令で、効率が良いです

lookupswitchはcase値が連続していないswitch文の複数条件分岐命令です(case値は連続していなくても、最終的にソートされます)

String型の場合は、まず対応するハッシュ値を探し、次にequals比較を行ってどのcaseを実行するかを決定します。

無条件分岐命令

無条件分岐命令は特定のバイトコード命令に分岐します

gotoは頻繁に使用されます。

jsrjsr_wretはあまり使用されなくなりました。

例外処理命令

throwで例外をスローするのはathrow命令に対応します:オペランドスタック上のすべての内容をクリアし、例外インスタンスを呼び出し元のオペランドスタックにプッシュします

try-catch/try-finally/throwsを使用すると、例外テーブルが生成されます。

例外テーブルは例外処理情報を保存します(開始位置、終了位置、バイトコード命令のオフセットアドレス、例外クラスの定数プール内のインデックスなどの情報)。

例外はスタックにプッシュされたり、例外テーブルに保存されたりします。

同期制御命令

synchronizedがメソッドに適用される場合、メソッドのアクセスフラグにはACC_SYNCHRONIZEDがあり、そのメソッドがロックを必要とすることを示します

synchronizedが特定のオブジェクトに適用される場合、monitorenterロック取得バイトコード命令とmonitorexitロック解放バイトコード命令に対応します

Javaのsynchronizedはデフォルトで再入可能ロックです

  • スレッドがロックが必要なオブジェクトにアクセスしようとするとき(monitorenterを実行)
    1. まずオブジェクトヘッダのロック回数を確認し、0の場合は未ロック状態を意味し、取得後にロック回数をインクリメントします
    2. 0でない場合、ロックを取得したスレッドが自分自身かどうかを確認し、自分自身であればアクセスを許可し、ロック回数をインクリメントします
    3. 0でなく、ロックを取得したスレッドが自分自身でない場合は、ブロック状態になります

スレッドがロックを解放する場合(monitorexitを実行)、ロック回数をデクリメントします。

なぜ2つのmonitorexitがあるのでしょうか?

プログラムが正常に実行される場合、通常は1つのmonitorenterに対して1つのmonitorexitが対応します。

もしプログラムがロックされたコード内で例外をスローし、ロックを解放しなかった場合、他のブロックされたスレッドが永遠にロックを取得できない状況が発生する可能性があります。

そのため、プログラムが例外をスローした場合(PCオフセットが15の命令に分岐)、実行を継続し、例外をスローする前にロックを解放する必要があります。

本記事ではバイトコード命令の後編として、メソッド呼び出しと戻り値、オペランドスタックのプッシュとポップ、制御転送、例外と同期に関連するバイトコード命令について詳しく解説しました。

メソッド呼び出し命令は静的、プライベート、インターフェース、仮想、動的メソッドなどに分類され、戻り値命令は主にi、l、f、d、aで始まるreturn命令がそれぞれ異なる型の戻り値を処理します。

オペランドスタックのポップ命令ではpop関連の命令がよく使用され、プッシュ命令(スタックトップ要素をコピーして挿入)ではdup関連の命令がよく使用されます。

制御転送命令の中で、条件分岐命令はスタックトップ要素を判断して分岐し、比較条件分岐命令は2つのスタックトップ要素の比較によって分岐を判断し、複数条件分岐はswitch文を満たすために使用され、例外発生時によくgoto無条件分岐が使用されます。

例外処理命令は例外をスローするために使用され、オペランドスタックをクリアし、例外を呼び出し元のオペランドスタックトップにプッシュします。

同期制御命令ではmonitorentermonitorexitがよく使用され、例外発生時のデッドロックを防ぐために、例外をスローする前にmonitorexitを実行します。

タグ: JVM バイトコード Java 仮想マシン 命令セット

7月10日 16:26 投稿