Java バックエンド テクノロジー: Java 仮想マシン スタックの探索

Java バックエンド テクノロジー: Java 仮想マシン スタックの探索

Java に精通している学生は、JVM がスタックベースであることを知っておく必要があります。しかし、この「スタック」は具体的に何を指すのでしょうか?仮想マシンスタックですか?この質問に答えるには、まず仮想マシン スタックの構造について説明する必要があります。

[[328790]]

仮想マシンスタック

仮想マシンスタックとは何ですか?

仮想マシン スタックのスタック要素はスタック フレームです。メソッドが呼び出されると、このメソッドを表すスタック フレームがスタックにプッシュされます。このメソッドが返されると、スタック フレームがスタックからポップされます。したがって、スタック フレームが仮想マシン スタックにプッシュされる順序は、メソッドが呼び出される順序になります。スタックフレームとは何ですか?スタック フレームは、メソッドの実行スペースとして理解できます。主に2つの部分から構成されます。 1 つの部分はローカル変数テーブルで、メソッドで定義されたローカル変数とメソッド パラメーターが格納されます。もう一方の部分はオペランド スタックで、オペランドを格納するために使用されます。 Java プログラムはコンパイルされるとバイトコード命令になります。バイトコード命令は形式的にはアセンブリ言語に似ていますが、アセンブリ言語とは異なります。アセンブリ命令のオペランドはデータ セグメントとレジスタに格納され、必要なオペランドはメモリまたはレジスタのアドレス指定を通じて見つけることができます。 Java バイトコード命令のオペランドはオペランド スタックに格納されます。 n 個のオペランドを持つ命令が実行されると、スタックの先頭から n 個のオペランドが取得され、命令の計算結果 (存在する場合) がスタックにプッシュされます。したがって、JVM 実行エンジンがスタックベースであると言う場合、「スタック」はオペランド スタックを指します。簡単な例を使って、アセンブリ命令と Java バイトコード命令の実行プロセスを比較してみましょう。たとえば、1 + 2 を計算する場合のアセンブリ命令は次のようになります。

  1. 移動 ax, 1 ;レジスタaxに1を入れる
  2. を追加、2; axと2の内容を加算し、axに格納する

JVM のバイトコード命令は次のとおりです。

  1. iconst_1 //整数1をオペランドスタックにプッシュします
  2. iconst_2 //整数2をオペランドスタックにプッシュします
  3. iadd //スタックの一番上の2つの数値を追加してポップし、結果をスタックにプッシュします

オペランド スタックはメモリ空間であるため、バイトコード命令は異なるマシン上のレジスタやマシン命令の違いを気にする必要がなく、プラットフォームの独立性が実現されます。

ローカル変数テーブル内の変数は直接使用できないことに注意してください。これらを使用する必要がある場合は、関連する命令を通じてオペランド スタックにロードし、オペランドとして使用する必要があります。たとえば、void foo() というメソッドがあり、そのコードは次のようになります: int a = 1 + 2; int b = a + 3; は、次のようなバイトコード命令にコンパイルされます。

  1. iconst_1 //整数1をオペランドスタックにプッシュします
  2. iconst_2 //整数2をオペランドスタックにプッシュします
  3. iadd // スタックの先頭にある 2 つの数値がポップされて加算され、結果がスタックにプッシュされます。実際、最初の3つのステップはコンパイラによって次のように最適化されます: iconst_3
  4. istore_1 //スタックの一番上の内容を、ローカル変数テーブルのインデックス1のスロットに格納します。これは、
  5. iload_1 // ローカル変数テーブルのインデックス1のスロットに格納されている変数値(3)をオペランドスタックにロードします。
  6. アイコンt_3
  7. iadd // スタックの一番上の2つの数値がポップされて加算され、その結果がスタックにプッシュされます
  8. istore_2 // スタックの一番上の内容を、ローカル変数テーブルのインデックス2のスロットに格納します。これは、bに対応するスペースです。
  9. return //メソッドの戻り命令、呼び出し元に戻る

ローカル変数テーブルとオペランド スタックの最大容量はコンパイル時に決定され、実行時には変更されないことに注意してください。また、ローカル変数テーブルのスペースを再利用できます。たとえば、命令の位置がローカル変数テーブル内の変数 a のスコープを超えている場合、新しいローカル変数 b を定義すると、b はローカル変数テーブル内の a のスペースを上書きします。

仮想マシン スタックを直感的に理解していただくために、他の人の図を使用しています (小さいフォントの Stack は仮想マシン スタック、Frame はスタック フレーム、Local variables はローカル変数テーブル、Operand Stack はオペランド スタックを表します)。

仮想マシンスタックによって発生する問題

上記のコードを読んだ後、いくつか疑問が湧くかもしれません: スロットとは何ですか?これらの指示はどういう意味ですか?明らかに最初に定義された変数であるにもかかわらず、対応するスロットのインデックス値がゼロから始まらないのはなぜですか?

私たちはこれらの問題を一つずつ解決していきます。

スロットとは何か

まず、スロットとは何でしょうか?スロットは、ローカル変数テーブル内のスペースの単位です。仮想マシンの仕様では、int、short、float などの 32 ビット以内のデータについては、1 つのスロットを使用して保存することが規定されています。 64 ビット データの場合、long、double などの 2 つの連続するスロットを使用してデータが格納されます。JVM は参照型変数の長さを指定しません。 32 ビットまたは 64 ビットであるため、1 つのスロットまたは 2 つのスロットを占有する場合があります。

JVMバイトコード命令

2 番目の質問です。これらの指示はどういう意味ですか?

命令フォーマット

まず、Java 命令の形式を理解する必要があります。 Java 命令はバイト単位で表されます。つまり、1 バイトが 1 つの命令を表します。たとえば、iconst_1 は 1 バイトを占める命令なので、当然 Java 命令の数は 256 を超えることはありません。実際、現在 200 を超える Java 命令が定義されています。命令は 1 バイトですが、独自のオペランドを持つこともできます。 JVM には putstatic という命令があり、特定の静的フィールドに値を割り当てるために使用されます。しかし、どのフィールドに値を割り当てるべきでしょうか?この命令だけでは説明できないため、オペランドを通じてのみ指定できます。 putstatic に続く 2 バイトはそのオペランドであり、ランタイム定数プール内の静的フィールドに対応するシンボリック参照を指すインデックス値です。シンボリック参照には、クラス、単純名、記述子などのフィールドの基本情報が含まれているため、putstatic 命令はどのクラスのどのフィールドに値を割り当てるかを認識します。

命令オペランドには 2 つのタイプがあります。1 つは命令内に埋め込まれ、通常は命令バイトの数バイト後にあります。もう 1 つはオペランド スタックに格納されます。これらを区別するために、前者を埋め込みオペランド、後者をスタックオペランドと呼びます。 2 つの違いは、埋め込まれたオペランドはコンパイル時に決定され、実行時には変更されないことです。命令と同様にクラス ファイル メソッド テーブルの Code 属性に格納されます。一方、オペランドは実行時に決定されます。つまり、プログラムの実行中に動的に生成されます。たとえば、putstatic 命令を考えてみましょう。これには、インデックス値 (前述) である埋め込みオペランドがあります。これは 2 バイトで構成され、putstatic に対応するバイトの後に続きます。また、オンスタックオペランドも存在し、これはオペランドスタックの最上部に配置されます。このオペランドは静的フィールドに割り当てられる値であり、対応するバイト数は静的フィールドの型によって決まります。静的フィールドの型が short、int、boolean、char、または byte の場合、オペランドはスタックの上位 4 バイトで構成される int 型である必要があります。型が float、double、または long の場合、オペランドは対応する型となり、スタックの上位 4、8、または 8 バイトで構成されます。静的フィールドが参照型の場合、オペランドの型もスタックの上位 8 バイトで構成される参照型である必要があります。

もう一つの例を挙げましょう。 iconst_ は命令のファミリを表し、整数 i をオペランド スタックに格納することを意味します。 i の範囲は (m1, 0, 1, 2, 3, 4, 5) です。ここで、m1 は -1 を表します。ここでの i は命令のオペランドではないことに注意してください (つまり、埋め込みオペランドやスタック内のオペランドではありません)。たとえば、iconst_1、iconst_2、iconst_3 はすべて 1 バイトで構成されるバイトコード命令です。 i は命令の「暗黙のオペランド」と見なすことができます。つまり、命令自体にオペランドが含まれています。整数 i が範囲 [-1, 5] を超える場合、バイトコード命令の 1 バイトにすべての整数を含めることは不可能であるため、iconst_ では表現できません。このとき、 bipush 命令が必要になります。この命令には、スタックの一番上に配置する整数を表すために使用されるバイトで構成される埋め込みオペランドがあります。整数がスタックの一番上に置かれると、符号ビットが拡張されて 32 ビット整数に変換されます。ただし、1 バイトではすべての整数を表すことはできません。整数値が 1 バイトで表現できる範囲を超える場合は、ldc 命令でのみ表現できます。この命令は、ランタイム定数プール内の Constant_Integer_info 型定数を指すインデックスを表す 1 バイトの埋め込みオペランドを持ちます。ランタイム定数プール内の整数をインデックスで参照すると、整数がどれだけ大きくても問題になりません。

コマンドのドキュメントを読む

魚を与えるよりも、魚の釣り方を教える方が良い。ここですべての手順を説明することは不可能なので、Oracle の公式 Web サイトにあるバイトコード手順に関するドキュメントの読み方を紹介します。文書のアドレスは次のとおりです:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html

astore 命令を例に挙げてみましょう。そのドキュメントでは次のように説明されています。

astore コマンド

説明と翻訳:

  • 太字の最初の行は命令の名前です。
  • 操作は命令の機能です。参照をローカル変数に格納します。
  • フォーマットは命令のフォーマットです。最初のバイトは astore という名前の命令で、2 番目のバイトは index という名前の命令の埋め込みオペランドです。 Forms は命令の 10 進数 (16 進数) コードを参照し、astore の 10 進数 (16 進数) コードは 58 (0x3a) です。
  • 操作スタックは、命令が実行される前と後のオペランド スタックの状態です。最初の行は命令が実行される前のオペランドの状態を表し、2 行目は命令が実行された後のオペランド スタックの状態を表し、矢印はスタックの先頭への方向を表します。 astore が実行される前に、スタックの最上部は astore のスタック オペランドであるオブジェクト参照 objectRef になります。実行後、objectRef がポップアウトされ、ローカル変数テーブルに格納されます。
  • 説明は、この命令の説明です。インデックスは符号なしバイトであり、このインデックスは、現在のスタック フレームのローカル変数テーブル内の特定の場所を指す必要があります。オペランド スタックの最上部の参照値は、returnAddress (メソッドの戻りアドレス) または reference (オブジェクト参照) である必要があります。この参照はポップされ、その値はローカル変数テーブル内のインデックス index のスロットに格納されます。
  • 注: Java で finally 句を実装する場合、astore 命令で使用されるオペランド型は returnAddress です。 astore に対応する aload 命令 (ローカル変数テーブルの参照値をスタックにプッシュする) は、オペランド スタックに returnAddress 型の値をロードできず、reference 型の値のみをロードできます。 aload と astore 間の非対称性は意図的です。 astore 命令を wide 命令と組み合わせて使用​​すると、符号なしの 2 バイト インデックスを使用してローカル変数テーブルから変数を取得できます。

ローカル変数テーブルの最初の変数

Java 言語の観点から見ると、静的メソッドとインスタンス メソッドの本質的な違いは、オブジェクトによって共有されるかどうかです。 JVM の観点から見ると、メソッド (静的メソッドまたはインスタンス メソッド) は実際にはオブジェクトによって共有され、インスタンス変数はオブジェクトに対してプライベートです。 JVM の場合、静的メソッドとインスタンス メソッドの本質的な違いは、特定のオブジェクトに関連付ける必要があるかどうかにあります。静的メソッドはクラス名を通じて呼び出すことができ、特定のオブジェクトに関連付ける必要はありません。一方、インスタンス メソッドはオブジェクトを通じて呼び出す必要があり、特定のオブジェクトに関連付ける必要があります。では、インスタンス メソッドと特定のオブジェクトはどのように関連付けられるのでしょうか?実のところ、それは非常に簡単です。コンパイル中に、コンパイラはメソッド レシーバーを暗黙的なパラメーターとしてインスタンス メソッドに渡します。このパラメータは、メソッド内では「this」と呼ばれる非常に馴染みのある名前を持っています。インスタンス メソッドがクラスのインスタンス変数や他のインスタンス メソッドにアクセスできるのは、暗黙のパラメーター「this」があるためです。たとえば、クラス A のメソッド b はインスタンス変数 x にアクセスする必要があります。インスタンス変数はオブジェクトに対してプライベートであるため、b が静的メソッドである場合、特定のオブジェクトへの参照がないため、どのオブジェクトのインスタンス変数 x にアクセスすればよいかわかりません。 b がインスタンス メソッドの場合、暗黙のパラメーター this によって、アクセスするインスタンス変数が this.x であることが判別されます。では、なぜ静的メソッドはクラスのインスタンス メソッドを呼び出すことができないのでしょうか?根本的な理由は、この参照が存在しないことです。インスタンス メソッドを呼び出すための前提条件は暗黙的なパラメーターを渡すことです。インスタンス メソッドには既にこの参照があるため、別のインスタンス メソッドに暗黙的なパラメーターとして渡すことができます。静的メソッドにはこの参照がなく、メソッド レシーバーを指す暗黙的なパラメーターを持つインスタンス メソッドを提供できないため、インスタンス メソッドを呼び出すことはできません。

上記のことを理解すれば、3 番目の質問は簡単に解決されます。定義したメソッドはインスタンス メソッドである void foo() であるため、特定のオブジェクトを指す暗黙的なパラメーター this が存在します。これは、ローカル変数テーブルの最初の位置、つまりインデックス 0 のスロットに格納されます。そのスコープはメソッドの最初から最後までであるため、ローカル変数テーブル内の位置は他の変数によって上書きされず、メソッドで定義する変数はローカル変数テーブルの後の位置にのみ配置できます。メソッドにパラメータ(暗黙的ではないパラメータ)がある場合、パラメータはこれに従って順番にローカル変数テーブルに格納されることに注意してください。パラメータのスコープもメソッド本体全体であるため、メソッドで定義されたローカル変数はパラメータの後にのみ配置できます。一般に、ローカル変数テーブル内の変数の格納順序は、this (インスタンス メソッドの場合) => パラメーター (存在する場合) => 定義されたローカル変数 (存在する場合) です。

仮想マシン スタックについては、これだけです。 Java 仮想マシンは完全な知識システムです。仮想マシン スタックを理解するだけでは十分ではありません。メモリ モデル、ランタイム定数プール、クラス ローディング モデルなど、ここでは詳しく説明されていない仮想マシンに関するその他の知識については、読者自身が学習して習得する必要があります。この記事は、JVM の学習に対する皆様の興味を刺激するものであり、また個人の学習記録や知識の要約としても役立ちます。 JVM の他の側面についての概要記事をいくつか書いて、後で皆さんと共有する予定です。

参考図書:

「Java仮想マシンの徹底理解」周志明著

Java 仮想マシン仕様

<<:  システム「Thanos」 - Java 仮想マシン

>>:  Red Hat がクラウドネイティブ開発をサポートし、K8S での Java アプリケーションを促進する Quarkus フレームワークを発表

推薦する

企業に必要なのはウェブサイトの最適化だけではありません

ウェブサイトの最適化が企業にもたらす利益がますます大きくなるにつれて、ウェブサイトの最適化の役割を認...

ユニクロ:オンラインとオフラインを融合した「型破りな」ゲームプレイで、最も成熟したO2Oモデルを創出

ユニクロといえば、北京や上海などの都市の主要ショッピングモールに店舗が頻繁に出店していることに加え、...

人民日報:WiFiやiPhoneなどの外国語は中国語の純粋さを損なう

グローバル化と情報化の発展に伴い、外国語の使用はますます広まっています。結果として、外国語の過剰使用...

4か月間降格されたサイトの記事を通常の状態に戻す方法

今日は木曜日、寝る前に、多くのウェブマスターと同じようにウェブサイトの状態を確認しました! 私のウェ...

オンラインストアの商品説明に記載すべき6つのこと

多くの Taobao 出店者は、商品詳細ページを飾るために美しい商品説明テンプレートを望んでいますが...

ウェブサイトのタイトルのキーワードを正しく書くにはどうすればいいですか?

Baidu の検索エンジン最適化では、ウェブサイトのタイトルの重要性は明らかです。ウェブサイトの t...

iwfhosting: 334 ドル、超ハイエンド サーバー、E7-4850/512g メモリ/18T ハードディスク

18 年間運営されている H4Y Technologies LLC が、特別価格で専用サーバー 3 ...

40% オフ プロモーション: photonvps - $3.57/KVM/512m メモリ/20g SSD/2T トラフィック/DDOS 保護

photonvps.com のクリスマス プロモーションが始まりました: 全アイテムが 40% オフ...

オンサイト最適化を使用して、新しいサイトのBaiduランキングを向上させる

私は半年以上自分のウェブサイトを構築しており、初心者レベルのウェブサイトビルダーと言えるでしょう。現...

ウェブサイト最適化診断とSEO監査業務

SEO 監査の関連作業内容を徐々に紹介してきました。この記事では、SEO 監査の関連作業と、最適化作...

Tencent Cloud: 超お得なプロモーション、年間38元から、さまざまな「ハイエンド」および安価なクラウドサーバーから選択可能

Tencent Cloud はほぼ常にプロモーションを行っていますが、最近開始されたプロモーションの...

微博と胡屋が興味コミュニティに参入

昨今、若者の「嗜好」がますます厳しくなる中、若者を本当に「満足」させることができる商品はますます少な...

ドメイン名にキーワードが含まれていることは、ウェブサイトのランキングに関係があるのでしょうか?

ドメイン名にキーワードが含まれていると、ウェブサイトのランキングに有利になります。ドメイン名にキーワ...

冬越し中のWeChatモーメントにご注意

10日前、「Win in China」の決勝戦を観ました。今でも一番鮮明に覚えているのは、ジャック・...