いわゆる「仮想マシン バイトコード実行エンジン」は、実際には、クラス ファイルに指定されたバイトコード命令に従って JVM のスタック インタープリターに基づく実行メカニズムです。簡単に言えば、JVM がバイトコード命令を解析し、実行結果を出力するプロセスです。次に、この部分を詳しく見てみましょう。
メソッド呼び出しの性質 「バイトコード実行エンジン」について説明する前に、まずスタック フレーム ベースのメソッド呼び出しがアセンブリ レベルからどのように機能するかを見てみましょう。 (IA32 CPU命令セットを例に挙げます) スタック フレーム データ構造は、IA32 プログラムでプロシージャ呼び出し (Java 言語ではメソッドと呼ばれる) をサポートするために使用されます。各プロシージャはスタック フレームに対応し、プロシージャ呼び出しはスタック フレームのプッシュとポップに対応します。ある瞬間には、スタックの最上部にあるスタック フレームのみが使用可能であり、これは実行中のメソッドのさまざまな状態を表します。最上位のスタック フレームは、スタック ポインターとフレーム ポインターの 2 つのポインターによって定義されます。これらは、それぞれレジスタ %ebp と %esp に格納されているスタック上のアドレスに対応します。スタックの一般的な構造は次のとおりです。 スタック ポインターは常にスタックの最上位要素を指し、スタック内の要素のエントリと終了を制御します。フレーム ポインターは、現在のスタック フレームの下部を指します。これはスタック全体の一番下ではなく、現在のスタック フレームであることに注意してください。 C コードの一部を見てみましょう。
これは非常に単純なコードです。いくつかのリンク コードを省略し、コア部分を残しながら、対応するアセンブリ コードをアセンブルして生成します。
まず、メイン関数のアセンブリ コードを見てみましょう。 main 関数の最初の 2 つのアセンブリ命令は sayHello の最初の 2 つの命令と同じなので、後者でそれらを紹介します。 subl 命令はレジスタ %esp のアドレスから 20 を減算します。これは、スタック ポインターが 20 バイト上方に拡張される (スタックが後方に拡張される)、つまり、現在のスタック フレームに 20 バイトが割り当てられることを意味します。次に、movl は値 20 をアドレス -4 (%ebp) に書き込みます。これは、レジスタ %ebp を基準としたフレーム ポインター位置の 4 バイト上にあります。 %ebp の値が 0x14 の場合、スタック アドレス 0x10 に 20 が格納されます。 次に、movl 命令はパラメータ age の値を取得し、それをレジスタ %eax に格納します。 ここでコア呼び出しメソッドが登場します。コンピュータには、次の命令の位置を指すプログラム カウンター (PC) があり、プログラムは他のメソッドを呼び出すことがよくあります。では、呼び出し前の状態を復元し、呼び出しが完了した後に実行を継続するにはどうすればよいでしょうか? ここでの解決策は、呼び出し命令の最初のステップで戻りアドレスをスタックにプッシュし、実行のために sayHell メソッドにジャンプすることです。ここでは、スタック プッシュ プロセスが 1 つの命令に統合されているため、スタック プッシュ プロセスを見ることはできません。 次に、sayHello メソッドの最初の命令にジャンプして実行を開始します。 pushl はレジスタ %ebp のアドレスをスタックにプッシュします。このとき、%ebp は前のスタック フレームのフレーム ポインタ アドレスになります。この操作は実際には保存アクションです。次に、movl 命令はフレーム ポインターをスタックの先頭であるスタック ポインターの位置にポイントし、スタック ポインターを 16 バイト上方に拡張します。 次に、値32と2323が異なるスタックアドレスに書き込まれます。フレーム ポインターに対する相対アドレスを計算できます。 後続の操作では、x と y をそれぞれレジスタ %eax と %edx に書き込み、次に add 命令で加算操作を実行して、それをレジスタ %eax に格納します。結果はスタックにプッシュされます。 離脱命令は、次の 2 つの命令の合計に相当します。
それはどういう意味ですか? スタック ポインターを、現在のスタック フレームの一番下であるフレーム ポインターの位置に戻し、スタックをポップします。この方法では、sayHello 全体が占有するスタック フレームは参照できなくなり、これは現在のスタック フレームを解放することと同じです。 ret 命令は、呼び出し前の状態を復元し、メイン メソッドの実行を継続するために使用されます。 IA32 メソッド呼び出し全体は基本的に上記のようになります。 64 ビット x86-64 では、16 個のレジスタが追加され、パラメータの計算と転送に最初にレジスタが使用されるため、効率が向上します。ただし、このスタックベースの保存方法と比較すると、「移植性が低い」という欠点があり、異なるマシン上のレジスタの使用方法が確実に異なります。したがって、Java は間違いなくスタックを使用します。 ランタイムスタックフレーム構造 Java では、1 つのスタック フレームが 1 つのメソッド呼び出しに対応し、メソッドに関係するローカル変数、オペランド、戻りアドレスなどはすべてスタック フレームに格納されます。各メソッドに対応するスタック フレームのサイズは、基本的にコンパイル後に決定されます。メソッド内で必要なローカル変数テーブルの大きさやオペランド スタックの深さなどの情報は、メソッドの Code 属性にすでに書き込まれています。したがって、実行時にはメソッドのスタック フレーム サイズが既に固定されており、メモリを直接計算して割り当てることができます。 ローカル変数テーブル ローカル変数テーブルは、メソッドの実行中に使用されるさまざまな変数とメソッド パラメータを格納するために使用されます。仮想マシンの仕様では、ローカル変数テーブルの容量は変数スロットを最小単位として使用すると規定されていますが、スロットの実際のスペース サイズは指定されていません。各スロットには、boolean、byte、char、short、int、float、reference などを格納できる必要があるとだけ書かれています。 私の理解では、スロットはブラックボックスに相当します。占有する具体的なバイト数は状況によって異なりますが、このブラック ボックスには明らかにあらゆるタイプの変数を格納できます。 ローカル変数テーブルはオペランド スタックとは異なります。要素にアクセスするためにインデックス メカニズムを使用しますが、これはオペランド スタックに入る方法と出る方法とは異なります。例えば:
逆コンパイルしてローカル変数テーブルを見てみましょう。 ご覧のとおり、ローカル変数テーブルの最初のエントリは this と呼ばれるクラス参照であり、ヒープ内の現在のオブジェクトへの参照を指します。次に、メソッド パラメーター、ローカル変数 x、y、z、str が続きます。 これは実際には、各インスタンス メソッドがデフォルトでパラメーター this を渡し、それが現在のクラスのインスタンス参照を指すことを間接的に示しています。 オペランドスタック オペランド スタックは、操作スタックとも呼ばれます。ローカル変数テーブルとは異なり、要素にアクセスするためにインデックス メカニズムは使用されず、標準のスタック操作、プッシュとポップ、先入れ先出しが使用されます。メソッド実行の開始時にはオペランド スタックは空です。メソッドがステップごとに実行されると、メソッドの実行が終了するまで、オペランド スタック内でプッシュ操作とポップ操作が継続して発生します。 オペランド スタックは、メソッド実行プロセスの非常に重要な部分です。メソッド実行プロセスのすべての中間結果は、オペランド スタックを使用して保存する必要があります。 返送先住所 メソッドが別のメソッドを呼び出した後、後続のメソッド本体の実行を継続するには、呼び出しポイントに戻る必要があります。そして、他のメソッドが呼び出される場所は「戻りアドレス」と呼ばれます。 CPU が他のメソッドを実行した後に元の呼び出しポイントに戻り、呼び出し元のメソッド本体を続行できるようにするには、特定の手段を使用する必要があります。 冒頭で紹介したアセンブリ コードと同様に、この戻りアドレスは、多くの場合、呼び出し元のスタック フレームに事前にプッシュされます。メソッド呼び出しが終了すると、スタックの一番上の要素が取り出され、後続のメソッド本体の実行エントリが取得されます。 メソッド呼び出し メソッド呼び出しはこの記事の中心的な内容です。多くの場合、仮想マシン命令は特定のメソッドの呼び出しを必要としますが、そのメソッドにはオーバーロードや上書きなどの問題がある場合があるため、仮想マシンがターゲットの呼び出しメソッドを決定する問題を解決します。では、仮想マシンはどのメソッドを呼び出すかをどのように決定するのでしょうか?これは、この段階で処理される唯一のタスクです。 まず、解析プロセスについて説明する必要があります。前回の記事から、クラスが初めてロードされるときに、定数プール内のシンボリック参照を直接参照に置き換える処理が解析フェーズ中に完了することがわかります。これには、メソッドのシンボリック参照を直接参照に変換するプロセスが含まれますが、これは一部のメソッドにのみ適用されます。一部のメソッドは実行時にのみ決定され、解決されません。クラス読み込みフェーズ中の解析プロセスを「静的解析」と呼びます。 では、どのメソッドが静的に解決され、どのメソッドが動的に解決される必要があるのでしょうか? たとえば、次のコード:
Object クラスには equals メソッドがあり、String クラスにも equals メソッドがあります。上記のプログラムは明らかに String の equals メソッドを呼び出します。したがって、Object クラスをロードし、equals シンボル参照をそのクラス自身の equals メソッドの直接参照に直接ポイントすると、上記の obj は常に Object の equals メソッドを呼び出します。そうすると、多態性は決して実現されなくなります。 クラスのロード時に静的に解析できるのは、「コンパイル時に既知で、実行時に変更されない」メソッドのみです。これらのメソッドには主に、private によって変更された private メソッド、クラスの静的メソッド、クラス インスタンス コンストラクター、および親クラスのメソッドが含まれます。 その他のすべてのメソッドは総称して「仮想メソッド」と呼ばれ、クラス読み込みの解析フェーズでは解析されません。これらのメソッドを呼び出すことに問題はありません。仮想マシンは、直接参照に基づいてメソッドのエントリを見つけることができます。しかし、「非仮想方式」は異なります。仮想マシンは、実際のメソッドを見つけるために特定の戦略を使用する必要があります。見てみましょう。 静的ディスパッチ まず、コードの一部を見てみましょう。
出力は次のようになります。
答えは正しかったでしょうか?これは非常に一般的な面接の質問であり、メソッドのオーバーロードとメソッドのディスパッチ ロジックに関する理解をテストします。以下で分析してみましょう: まず、「静的型」と「実型」という 2 つの概念を導入する必要があります。静的型とは、変数の最外層にラップされた型を指します。たとえば、上で述べた父親はいわゆる静的なタイプですが、息子や娘は実際のタイプです。 コンパイラはバイトコード命令を生成するときに、変数の静的型に基づいて適切なメソッドを呼び出すことを選択します。上記の例では、次のようになります。 これら 2 つのメソッドは、メイン関数で 2 回呼び出される sayHello メソッドですが、渡されるパラメーターの型は同じ Father であることがわかります。つまり、呼び出されるメソッドは同じで、どちらも次のメソッドです。
つまり
メソッド実行バージョンを特定するために静的型に依存するすべてのディスパッチ アクションは「静的ディスパッチ」と呼ばれ、メソッドのオーバーロードは静的ディスパッチの典型的な表現です。ただし、静的ディスパッチでは実際の型は考慮されず、静的型に基づいてメソッド呼び出しのみが実行されることに注意してください。 ダイナミックディスパッチ
出力:
当然、サブクラスの sayHello メソッドが最後に呼び出されます。生成されたバイトコード命令呼び出しを見てみましょう。 見ましたか?コンパイラによって生成されたメソッド呼び出し命令は、静的型の対応するメソッドを呼び出すことを選択しますが、最終結果が実際の型の対応するメソッドを呼び出すのはなぜでしょうか? 型インスタンスの特定のメソッドを呼び出すときは、まず現在のインスタンスをオペランド スタックにプッシュし、次に、invokevirtual 命令で次の手順を完了してメソッドを呼び出す必要があります。
したがって、ここでの例ではサブクラス Son の sayHello メソッドを呼び出していることは自明です。 仮想マシンがクラス内の指定されたメソッドをなぜ正確かつ効率的に検索できるのかについては、仮想マシンごとに実装が異なりますが、最も一般的なのは「仮想メソッド テーブル」を使用していることです。この概念も比較的単純で、各タイプごとにメソッド テーブルが維持され、現在のタイプのすべてのメソッドの説明情報が記録されます。したがって、仮想マシンがメソッドを取得するときは、メソッド テーブルから検索するだけで済みます。現在のタイプのメソッド テーブルにメソッドが含まれていない場合は、親クラスのメソッド テーブルを検索します。 動的型機能のサポート 動的型付け言語の主な特徴は、実行時に型チェックが行われることです。つまり、コンパイル中、コンパイラは変数の型や、呼び出すメソッドが存在するかどうかを気にしません。例えば:
Java では、変数 obj の静的型が Object であることをコンパイラがチェックし、Object クラスに subString メソッドがないため、この 2 行のコードはコンパイラを通過できず、エラーが報告されます。 動的型付け言語であれば、このコードは問題ありません。 静的言語はコンパイル時に変数の型をチェックし、厳密なチェックを提供しますが、動的言語は実行時に変数の実際の型をチェックし、プログラムの柔軟性を高めます。それぞれに長所と短所があります。静的言語の利点はセキュリティであり、欠点は柔軟性の欠如です。動的言語の場合はその逆になります。 JDK1.7 では、Java の動的機能をサポートするために、invokedynamic 命令と java.lang.invoke パッケージという 2 つの方法が提供されています。両者の実装方法は類似しており、後者の基本的な内容のみを紹介します。
出力:
ご覧のとおり、obj の静的型は Object ですが、この方法ではコンパイラの型チェックをバイパスし、実行時に指定したメソッドを直接実行できます。 非常に複雑なので、具体的な実装方法は説明しません。機会があれば、それについて学ぶための別の記事を書きます。いずれにせよ、この方法では、変数の静的型を気にする必要はありません。呼び出したいメソッドがあれば、実行時に直接呼び出すことができます。 要約すると、HotSpot 仮想マシンはオペランド スタックに基づいてメソッドを解釈して実行します。基本的に、操作の中間結果やメソッド パラメータなどはすべて、スタックへの入出力操作とともに取得または保存されます。このメカニズムの最大の利点は、その優れた携帯性です。レジスタベースの実行メカニズムとは異なり、基盤となるハードウェアに大きく依存するため、クロスプラットフォーム化が容易ではありません。しかし、その欠点も明らかです。つまり、同じ操作を完了するには、比較的多くの命令が必要になります。 |
>>: NFV におけるクラウドネイティブ VNF の重要性
北京時報(記者:顧孟林 李斌)昨日、一部メディアは、百度傘下の動画サイト「iQiyi」が百度を通じて...
従来のデータ センター システムを使用する企業は、ファイアウォールの外側のシステムによる IT 資産...
O2Oは間違いなく中国の電子商取引市場におけるダークホースであり、近年の急速な拡大と成長は中国のO2...
2018年最もホットなプロジェクト:テレマーケティングロボットがあなたの参加を待っていますインターネ...
記事も長い間書いてないし、ブログも長い間更新してない。ちょっと退廃的な気分です。ハハ、今日は仕事が終...
仮想マシンがなければ、単一のオペレーティング システムがすべてのハードウェア リソースを占有しますが...
ブランドマーケティングは、高度なマーケティング手法です。ブランドマーケティングとは、消費者の製品に対...
weloveservers は、ほぼ 1 年前からある新しい VPS プロバイダーです。最も有名なの...
WeChat インタラクティブ マーケティングの別の言い方は、実際には感情マーケティングです。これは...
Raksmart は 10 月に新しいクラウド サーバー プロモーションを開始し、すべてのクラウド ...
1. Qvodの社長である王欣は1か月近く行方不明になっており、妻は浴室に隠れたまま姿を現していない...
フォーラムの管理は、常にモデレーターにとって大きな問題でした。モデレーターの考えは、管理が厳しすぎる...
5月29日、北京時間早朝、Googleは米国サンフランシスコで年次開発者会議を開催した。会議で、Go...
電子商取引の運営には442ルールがあります。つまり、成功の 40% はマーケティングターゲット、40...
我が記者顧暁宇は昨日、フォーブス誌の中国版が2011年に最も利益を上げたモバイルインターネット企業3...