[Sticky JVM] 面接の質問がきっかけで「スタックフレーム」が登場! ! !

[Sticky JVM] 面接の質問がきっかけで「スタックフレーム」が登場! ! !

[[387780]]

序文

最近、小農の友人である小勇は仕事を探しています。彼は今年の初めから、一歩踏み出して現実的な仕事を見つけたいと考えている。シャオヨンも面接に行ってきました。最近のインタビューでは、さまざまな根本原理が次々と出てくると言わざるを得ません。シャオヨンさんは面接でこんな質問を受けました。彼はうまく答えられなかったために面接で不合格となり、大きなショックを受けました。兄として、私は彼を慰めなければなりません。シャオヨンが再び面接に失敗した原因は、どのような質問だったのでしょうか?どうしてまた言ったのか不思議です。あまりにも嬉しくて、人々は泣きたくなるのです。ハハハ、本題に戻って見てみましょう!

シャオヨンは面接官の前でまっすぐに座った。これはすでにシャオヨンにとって5回目のインタビューだった。以前のインタビューでは何の反応も得られなかったため、シャオヨンさんは少し不安になった。しかし、今回シャオヨンはしっかり準備して来た。彼は以前の面接でどう答えたらよいか分からなかった質問のほとんどを見直しました。今回は大きな問題にはならないはずだと彼は考えた。

シャオヨンはこれまでの基本的な質問にきちんと答えました。面接官は彼の基礎知識がかなり優れていると見て、さらに詳しい質問をしました。

面接官: 履歴書を見ると、JVM に精通していると書かれていますね。次の質問をさせていただきます。まず、a = a ++; を実行した結果はどうなるでしょうか。そして a = ++a;?

  1. パブリッククラスTest1{
  2. 公共 静的void main(String[] args) {
  3. 整数a = 88;
  4. a = a++;
  5. // a = ++a;
  6. システム.out.println (a) ;
  7. }
  8. }

シャオヨンは思った。「これは簡単なことじゃないか?」どうして私はこれを知らないのでしょうか?そこで小勇は軽蔑の笑みを浮かべ、こう言った。「a = a++;出力結果は88、a = ++aです。 9です。とても難しいだろうなと思いましたが、これがそれでしょうか?この種の質問をもう一度してください。

インタビュアー:彼は無表情で「なぜこのような結果になったかご存知ですか?」と言いました。

小勇:本当に来たんだね。難易度が上がりました。あなたには何かスキルがあるようですね。幸いにも私は十分に準備していたので、そうでなければ今日この質問で行き詰まっていたでしょう。 a++ は、まず a を計算し、次に ++ を計算します。 a++ 演算はセミコロンが終了した場合にのみ実行されます。したがって、代入演算を行うと、a++ は依然として 88 なので、a に代入されると、これも 88 になります。セミコロンが終了したときのみ、a++ は 9 になります。++a は、最初に ++a を計算することです。セミコロンで終わっているかどうかに関わらず、この時点での値はすでに 9 です。なので、値を代入すると a が 9 になり、出力結果も 9 になります。これでもう何も言うことはありませんね!

面接官は顎に手を当てて、ゆっくりとこう言いました。「この操作は JVM メモリ内でどのように実行されるのですか?」

シャオヨン:それはどのように機能しますか?これが根本的な原理ではないでしょうか?スクリプトはこのように開発されませんでした。私はこれを一度も理解できませんでした。 。 。 。 Xiaoyong: 彼はためらいながら、これについてはこれまで学んだことがなく、基礎となる実装についてもよくわかっていないと言いました。

面接官は軽蔑したような笑みを浮かべ、「では、今日の面接はこれで終わりです。何かあれば人事部から連絡します」と言いました。

シャオヨン:!$%@#&*

理解できないなら学ぶ

上記の Xiaoyong 氏の発言を聞いて、面接官はメモリ内の実行時データに関する知識をテストする必要があることが大体わかりました。理解できないなら、学んでください。何かに遭遇しても慌てないでください。上記の面接の質問の本質を本当に理解したいのであれば、前提となる知識を少し補足する必要があります。まず、次の図を見てみましょう: クラスのライフサイクル:


上図では、まず .class ファイルがメモリに読み込まれ、メソッド領域 (Perm Gen) に格納されます。最終製品は Class オブジェクトです。次に、正しいデータ構造があるかどうかを確認します。 JVM はクラスの静的変数にメモリを割り当て、デフォルトの初期値を設定します。クラスのバイナリ データ内のシンボリック参照は、直接参照に置き換えられます。クラスの静的ステートメント ブロックを実行するために、JVM はまず親クラスを初期化します。 JVM 仮想マシンに実行された後、ランタイム エンジンに入り、最終的にランタイム エンジン内で実行されます。動作中のメモリ内の状況はどうなっていますか?これが今回の議論の焦点である実行時データ領域である。

ランタイムデータ領域

Java 仮想マシン ランタイム データ領域:


1.1 プログラムカウンタ

プログラム カウンターは、現在のスレッドによって実行されたバイトコードの行番号を示すインジケーターと考えることができる小さなメモリです。 Java 仮想マシンのマルチスレッドは、スレッドを順番に切り替えてプロセッサ実行時間を割り当てることによって実装されるため、プロセッサは任意の時点で 1 つのスレッド内の命令のみを実行します。そのため、スレッド切り替え後に正しい実行位置を復元するために、各スレッドには独立したプログラム カウンターが存在します。スレッドが Java メソッドを実行している場合、このカウンターは実行中の仮想マシン バイトコード命令のアドレスを記録します。ネイティブ メソッドが実行されている場合、カウンター値は空になります。

効果:

1. バイトコード インタープリタは、プログラム カウンタを変更して命令を 1 つずつ読み取り、コード フロー制御を実現します。たとえば、順次実行、選択、ループ、例外処理など。2. マルチスレッドの場合、プログラム カウンターは現在のスレッド実行の場所を記録するために使用されます。スレッドが戻されると、スレッドが前回どこで実行されたかを知ることができます。

特徴:

  1. 小さなメモリスペース
  2. スレッドはプライベートで、各スレッドにはプログラムカウンタがある
  3. OutOfMemoryErrorが発生しない唯一のメモリ領域です
  4. ライフサイクルはスレッドの作成時に作成され、スレッドの終了時に終了します。

1.2 Java仮想マシンスタック

Java 仮想マシン スタックもスレッド専用であり、そのライフ サイクルはスレッドのライフ サイクルと同じです。仮想マシン スタックは、Java メソッド実行のメモリ モデルを記述します。各メソッドは実行時にスタック フレームを作成し、ローカル変数テーブル、オペランド スタック、動的リンク、メソッド終了などの情報を格納します。メソッドを呼び出して実行するまでのプロセスは、スタック フレームをプッシュして仮想マシンにポップするまでのプロセスに対応します。

ケーススタディを見てみましょう。

  1. パブリッククラスTestStack{
  2.  
  3. 公共 静的void main(String[] args) {
  4. 新しい PlayRice().print();
  5. }
  6. }
  7.  
  8. クラス PlayRice{
  9.  
  10. パブリックvoid fun(){
  11. システム。 out .println( "努力する人、頑張る人、努力する人はみんな優秀な人です!!!" );
  12. }
  13.  
  14. パブリックボイドプリント(){
  15. 楽しい();
  16. }
  17. }

Java のメモリ領域は、多くの場合、ヒープ メモリとスタック メモリに分割されます。この区分は、従来の C および C++ プログラムの内部構造から直接継承されていますが、Java 言語では明らかに不適切です。 Java のメモリ領域はこれら 2 つよりもはるかに複雑です。ただし、この区分が人気を博していることは、プログラマが最も注意を払い、オブジェクトのメモリ割り当てに最も密接に関連する領域がヒープとスタックであることを簡潔に説明しています。スタックは通常、仮想マシンを指しますが、多くの場合、仮想マシン スタック内のローカル変数テーブルの一部を指します。ローカル変数テーブルには、さまざまな基本データ型 (boolean、byte、char、short、int、float、long、double) と、コンパイル時に認識されるオブジェクト参照が格納されます。

Java 仮想マシン仕様では、この領域に関して 2 つの例外が定義されています。1. スレッドによって要求されたスタックの深さが仮想マシンで許可されている深さより大きい場合、StackOverflowError がスローされます。 2. Java 仮想マシン スタックが動的に拡張され、拡張中に十分なメモリが要求されない場合、OutOfMemoryError がスローされます。

1.3 ネイティブメソッドスタック

ネイティブ メソッド スタックと仮想マシン スタックが果たす役割は非常に似ています。それらの違いは、仮想マシン スタックは仮想マシンによって実行される Java メソッド (つまり、バイトコード) を提供するのに対し、ネイティブ メソッド スタックは仮想マシンによって使用されるネイティブ メソッドを提供することです。

仮想マシン仕様では、ローカル メソッド スタック内のメソッドの言語、使用法、データ構造に関する強制的な規制はないため、特定の仮想マシンで自由に実装できます。一部の Java 仮想マシン (ホットスポット仮想マシン) では、ローカル メソッド スタックと仮想マシン スタックを直接 1 つに結合します。仮想マシンと同様に、ネイティブ メソッド スタックも StackOverflowError および OutOfMemoryError 例外をスローする可能性があります。

1.4 ヒープ

Java ヒープは、仮想マシンによって管理されるメモリの最大の領域です。 Java ヒープはすべてのスレッドで共有されるメモリ領域であり、仮想マシンの起動時に作成されます。このメモリ領域の唯一の目的は、オブジェクト インスタンスを格納することです。 Java の世界のほぼすべてのオブジェクト インスタンスはここに割り当てられます。

Java 仮想マシン仕様における Java ヒープの説明は、「すべてのオブジェクト インスタンスと配列はヒープ上に割り当てられる必要があります。」です。 Java はガベージ コレクターによって管理されるメモリ領域です。メモリ回復の観点から見ると、現代のガベージ コレクターのほとんどは世代別コレクション理論に基づいて設計されているため、Java ヒープには「新しい世代、古い世代、永久世代、Eden、Survivor」が頻繁に表示されます。

Java 仮想マシン仕様によれば、Java ヒープは物理的に不連続なメモリ空間に配置できますが、論理的には連続していると見なされます。これは、ディスク領域を使用してファイルを保存する場合と同様であり、各ファイルを継続的に保存する必要はありません。ただし、大きなオブジェクト (通常は配列オブジェクト) の場合、ほとんどの仮想マシン実装では、単純さと効率的なストレージのために連続したメモリ領域が必要になる可能性があります。

Java ヒープは固定サイズまたは拡張可能として実装できますが、現在主流の Java 仮想マシンはすべて拡張可能な形式 (パラメータ -Xmx および -Xms によって設定) で実装されています。 Java ヒープ内にインスタンス割り当てを完了するためのメモリがなく、ヒープを拡張できなくなった場合、Java 仮想マシンは OutOfMemoryError 例外をスローします。

1.5 方法領域

メソッド領域は、Java ヒープと同様に、すべてのスレッドで共有されるメモリ領域です。仮想マシンによってロードされた、ジャストインタイムコンパイラによってコンパイルされた型情報、定数、静的変数、コードキャッシュなどのデータを格納するために使用されます。 Java 仮想マシン仕様では、メソッド領域はヒープ論理部分として説明されていますが、Java ヒープと区別するために「非ヒープ」という別名が付けられています。

Java 仮想マシン仕様では、メソッド領域に対する制約が非常に緩やかです。 Java ヒープと同様に、連続したメモリを必要とせず、サイズを固定または拡張できます。ガベージ コレクションを実装しないことも選択できるため、この領域ではガベージ コレクションの動作はほとんど発生しません。この領域のメモリリサイクル対象は主に定数プールと型のアンロードのリサイクルですが、この領域のリサイクル効果はあまり満足できるものではありません。

メソッド領域が新しいメモリ割り当て要件を満たすことができない場合、OutOfMemoryError 例外がスローされます。

1.6 ランタイム定数プール

ランタイム定数プールはメソッド領域の一部です。クラス ファイルには、クラス バージョン、フィールド、メソッド、インターフェイス、およびその他の説明情報に加えて、コンパイル中に生成されるさまざまなリテラルとシンボル参照を格納するために使用される定数プール テーブルも含まれています。このコンテンツは、クラスがロードされた後にメソッド領域の実行時定数プールに保存されます。

Java 仮想マシンでは、クラス ファイルの各部分 (定数プールを含む) の形式に関して厳格な規制があります。たとえば、各バイトは、仮想マシンによって認識、ロード、実行されるために、仕様の要件に従ってデータを格納するために使用する必要があります。ただし、Java 仮想マシン仕様には、ランタイム定数プールに関する詳細な要件は規定されていません。さまざまなプロバイダーによって実装された仮想マシンは、独自のニーズに応じてこのメモリ領域を実装できます。ただし、一般的には、クラス ファイルに記述されたシンボリック参照を保存するだけでなく、シンボリック参照から変換された直接参照もランタイム定数プールに保存されます。

クラス ファイル定数プールと比較したランタイム定数プールのもう 1 つの重要な機能は、動的であることです。 Java 言語では、定数をコンパイラによって生成する必要はありません。つまり、クラス ファイルに事前に配置された定数プールの内容だけが、メソッド領域内の実行時定数プールに入ることができるわけではありません。実行時に新しい定数をプールに配置することもできます。この機能は、開発者が String クラスの intern() メソッドでよく使用します。

ランタイム定数プールはメソッド領域の一部であるため、当然メソッド領域のメモリによって制限されます。定数プールがメモリに適用できなくなると、OutOfMemoryError 例外がスローされます。

1.7 直接記憶

ダイレクト メモリは、仮想マシンのランタイム データ領域の一部ではなく、Java 仮想マシン仕様で定義されているメモリ領域でもありません。ただし、この部分は頻繁に使用されるため、OutOfMemoryError 例外が発生する可能性もあります。 JDK1.4 では、NIO (New Input/Output) クラスが新たに追加され、チャネル (Channel) とバッファ (Buffer) に基づく I/O メソッドが導入されました。ネイティブ関数ライブラリを使用して、オフヒープ メモリを直接割り当てることができます。次に、このメモリへの参照として、Java ヒープ内に格納されている DirectByteBuffer オブジェクトを介して操作します。

1.8 要約

下の図から、各スレッドには独自のプログラム カウンター、Java 仮想マシン スタック、ローカル メソッド スタックがありますが、ヒープとメソッド領域は共有されていることがわかります。各スレッドに独自のプログラム カウンターがあるのはなぜですか?スレッドが実行されると、CPU は別のスレッドに切り替えて実行することをすでに上で説明しました。他のスレッドを実行して切り替えると、現在のスレッドの実行位置がわかります。


面接の質問を理解する

最初に話した面接の質問に戻りましょう。まずi=i++=88を見てみましょう。内部的にはどのように実行されますか?その命令がどのように動作するかを確認する必要があります。 Jclasslib を使用してバイナリ コードを解析し、メイン メソッドをポイントすることができます。

1.1 Jclasslibをインストールする

まず、Jclasslib をインストールする必要があります。インストールは以下のように成功します。

1.2 バイトコードの表示

まず、メインメソッドを実行し、そのクラスの内容をロードし、表示 -> Jclasslibでバイトコードを表示をクリックします。

メイン メソッドには 2 つのテーブルが記録されます。

表 1: LineNumberTable は行番号を記録します。表 2: LocalVariabletable は、メソッド内で使用される変数を含むローカル変数テーブルです。最初のものは args で、2 番目のものは a です。したがって、ローカル変数テーブルは、現在のメソッドのこのスタック フレームで使用されるローカル変数を参照します。

a = a++;

次に、a = a++; の具体的な実行プロセスを見てみましょう。

  1. 0 ビプッシュ 88
  2. 2 istore_1
  3. 3 ロード1
  4. 4 増分 1 × 1
  5. 7 アイストア_1
  6. 8 getstatic #2 <java/lang/ System.out >
  7. 11 ロード1
  8. 12 仮想関数 #3 <java/io/PrintStream.println>
  9. 15戻る 

指示の意味がわからない場合は、対応する指示をクリックすると、ブラウザがその指示の詳細な説明を直接見つけます。

まず、bipush 88 と istore_1 を見てみましょう。対応する値は int a = 88; です。 iload+1 は 89 に等しいです。したがって、89 を割り当てても、89 のままです。

  • bipush 88 はバイトをスタックにプッシュすることを意味します。 88 はバイト値として扱われ、自動的に Int 型に拡張され、スタックに配置されます。 88 がローカル変数テーブルに配置され、入力結果は 88 になります。
  • 2 番目の命令 istore_1 は、スタックの一番上の数字をポップし、インデックス値 1 でローカル変数テーブルに格納します。a の値は、添え字値 1 でローカル変数テーブル内にあります。今、88 がスタックの一番上に格納されました。ここで 88 が取り出され、 a に配置されます。したがって、これら 2 つの文が完了すると、次の図に示すように、対応する int a = 88 が完成します。

iload_1: はローカル変数から int をロードすることを意味します。つまり、次の図に示すように、ローカル変数テーブルから値を取得してスタックに格納することを意味します。

iinc 1 by 1: a++ 演算を実行し、ローカル変数テーブルの値 88 に 1 を加算して、値は 89 になります。

istore_1: a = a++ 演算を実行します。 a++ 演算は以前に実行されています。このとき、a++ の a は int a に代入されるため、スタック内のデータはローカル変数テーブルに代入されます。したがって、この時点でローカル変数テーブル内のデータは 88 になります。


したがって、最終結果は 88 になります。

a = ++a;

バイトコード命令:

  1. 0 ビプッシュ 88
  2. 2 istore_1
  3. 3 増分 11
  4. 6 ロード1
  5. 7 アイストア_1
  6. 8 getstatic #2 <java/lang/ System.out >
  7. 11 ロード1
  8. 12 仮想関数 #3 <java/io/PrintStream.println>
  9. 15戻る 

bipush 88 と istore_1: この文は実際には int a = 88 を完了し、最初に 88 をスタックにプッシュし、次にそれをポップしてローカル変数テーブルに割り当てます。


iinc 1 by 1: ++a 演算を実行するので、この時点でローカル変数テーブルのデータは 89 になります。


iload_1: このとき、ローカル変数テーブルの値がスタックにプッシュされます。


istore_1: このとき、a = ++a という演算が実行され、a の値が int a に割り当てられます。スタック内のデータ自体は 89 なので、最終的に印刷される結果も 89 になります。


補足: int a = 250 に設定すると、以下の値は sipush になります。250 は 127 より大きく、127 はバイトが表すことができる最大値より大きいため、表示されるバイナリ値は sipush になり、s は short を表します。

要約する

これまで学ぶことを諦めていませんでしたか?実際、何かを学んだとき、それをどのように使うかはわかっていても、細かい点については慎重に考える必要があり、非常に退屈になることがあります。その原理を理解すると、私たちは悟りを開いたと感じるでしょうか?小農はそうするでしょう、あなたはどうですか?

私は小さな農家です。私は真実を恐れません。私のできることには限界がありません。さらなる進歩にはさらなる喜びがあります。さあみんな!

<<:  IoTの成長が牽引するエッジコンピューティングの4つの垂直アプリケーション

>>:  Webhook を使用した Kubernetes のタグベースの権限制御の実装

推薦する

グラフィカル分散コンセンサスアルゴリズム

本日の記事では、グラフを使用して分散一貫性の実装原則を深く研究し、理解します。まず、自己を見つめ直す...

【ゲームプロモーション】大手ゲームメーカーのやり方を見てみよう!マーケティングプロモーション戦略

2014年の最後の月を振り返ってみましょう。一秒一秒が重要です。2014年12月は血みどろの戦いでし...

ガートナーのクラウドAI開発サービスに関するマジック・クアドラントレポートが正式に発表され、テンセントが2年連続で選出

本日、国際的に権威のある調査機関であるガートナーによる2021年版「クラウドAI開発サービス分野のマ...

Centos6にownCloudをインストールする

ownCloud は、無料でオープンソースであり、個人または商用ストレージの代替ソリューションとして...

北京市民は「北京政府サービス」アプレットを使用して1,000以上のサービスを処理できます。

現在までに、「北京政府サービス」ミニプログラム(「北京通」ミニプログラム)のサービス項目数は1,00...

2018年ワールドカップでブランドマーケティングを実施するには?マーケティング計画は準備できていますか?

20日後には、何百万人ものファンの注目を集めるワールドカップが始まります。あなたの「ワールドカップマ...

入札広告から電車内プロモーション、オンラインプロモーションからモバイルマーケティングまで

----Yimake の講師がインターネット マーケティング コースのアップグレードについて語るはじ...

キングソフトのアプリ広告禁止が論争を巻き起こし、有料アプリの変更につながる可能性

ポータルサイトやQQソフトウェアの広告が一時的にブロックされた後、新進のアプリ開発者も同じ問題に直面...

Armorshark ニュージャージー G ポート 超低価格 VPS/openvz/kvm

Armorshark は 2010 年から VPS 事業に携わっています。TigerS.com とい...

cloudserver.netはどうですか?ロサンゼルスデータセンターのVPSのレビュー

cloudserver.net がここ 2、3 年で人々の目に留まり、注目を集めるようになった理由は...

モモ版「Douyin」がリリース。動画ソーシャルアプリ「Duiyan」は人気になれるか?

今年、 Momoの新年の大ヒット作「Duiyan APP」が先日正式にリリースされました。今回、Mo...

分散 |知っておくべき負荷分散

[[378668]]最近、友人がバックグラウンドでメッセージを残し、負荷分散に関する記事を書くように...

IDC: パブリッククラウドのIaaSとPaaSの収益は2025年に4,000億ドルを超える

業界データIDC は、パブリック クラウド IaaS および PaaS 市場の総収益が 2021 年...