JVM - クラスのロードとガベージ コレクション

目次

序文

JVM の概要

JVMのメモリ領域分割

JVMクラスロードメカニズム

1.負荷

保護者の委任モデル

2. 検証

検証オプション

3. 準備する

4. 分析

5. 初期化

トリガークラスのロード

JVM ガベージ コレクション戦略 GC

1: 誰がゴミなのかを見つけ出す 

1. 参照カウント

2. アクセシビリティ分析 (このソリューションは Java で採用されています)。

2: ガベージ オブジェクトを解放する

3 つの典型的な戦略

JVM実装のアイデア


序文

JVM を学習するとき、実際には多くの内容が含まれていますが、そのほとんどは定型的なものです。JVM を徹底的に理解したい場合は、JVM に関する多くのソース コードを読む必要があります。JVM のソース コードは書かれています。 C++で。さらに詳しく学びたい場合は、「Java 仮想マシンの徹底理解」という書籍を読んでください。

この記事では、JVM における一般的な面接の質問に焦点を当てます。

JVM の概要

JVMとはJava Virtual Machineの略で、Java仮想マシンのことです。
仮想マシンとは、ソフトウェアによってシミュレートされ、完全なハードウェア機能を備え、完全に分離された環境で実行される完全なコンピューター システムを指します。
一般的な仮想マシン: JVM、VMwave、Virtual Box。
JVM と他の 2 つの仮想マシンの違いは次のとおりです。

  1. VMwave と VirtualBox はソフトウェアを通じて物理 CPU の命令セットをシミュレートし、物理システムには多数のレジスタが存在します。
  2. JVM は Java バイトコードの命令セットをソフトウェアでシミュレートしますが、JVM では主に PC レジスタのみが予約され、その他のレジスタは削除されます。

JVM は、現実には存在しないカスタマイズされたコンピューターです。

JVMのメモリ領域分割

JVM は実際には Java プロセスであり、Java プロセス、つまり JVM は、Java コードが使用するためにオペレーティング システムから大きなメモリ領域を申請します。

JVM は、オペレーティング システムが要求するメモリ空間をさらに分割し、分割された各空間の異なる用途を提供します。

このうち中核となるのはスタック、ヒープ、メタデータ領域(メソッド領域)です。

  • 仮想マシン スタックは Java コードによって使用され、主にいくつかのローカル変数を保存し、メソッド間の呼び出し関係を維持します。
  • ネイティブ メソッド スタックは、JVM 内のネイティブ メソッドによって使用されます。
  • 新しいオブジェクトとメンバー変数はヒープに保存されます。
  • プログラムカウンタに格納されるのはメモリアドレスであり、このメモリアドレスは次のバイトコードが実行されるアドレスであり、現在のプログラムが実行する命令を記録する役割を果たします。

1 つの JVM にはヒープ領域とメタデータ領域のコピーが 1 つだけ存在することに注意してください。つまり、複数のスレッドがヒープ領域とメタデータ領域を共有します。

スタック (ローカル メソッド スタックと仮想マシン スタック) とプログラム カウンターのコピーが複数あり、各スレッドに 1 つずつ存在します。

JVM のスレッド操作とオペレーティング システムのスレッド操作の間には 1 対 1 の関係があります。つまり、Java コードで作成されたすべてのスレッドには、オペレーティング システム内でそれに対応するスレッドが存在します。

ここでのインタビューの質問は主に、特定の変数またはオブジェクトが JVM のどの領域にあるかを判断することです。

たとえば、次のコード:

void func() {
    Test t1 = new Test();
}

上記のコードでは、メソッド内で Test オブジェクトをインスタンス化します。

 func メソッドは、メタデータ領域にバイナリ命令として格納されます。

t1 変数がメソッド内で定義されているため、これはローカル変数であり、ローカル変数はスタックに格納されていることがわかります。

そして new Test(); このオブジェクトの本体はヒープ上にあります。

実際、ここでの JVM 領域に関するインタビューの質問と同様に、JVM の各領域に何が格納されているかを知る必要があるだけです。

  • 仮想マシン スタックは Java コードによって使用され、主にいくつかのローカル変数を保存し、メソッド間の呼び出し関係を維持します。
  • ネイティブ メソッド スタックは、JVM 内のネイティブ メソッドによって使用されます。
  • 新しいオブジェクトとメンバー変数はヒープに保存されます。
  • プログラムカウンタに格納されるのはメモリアドレスであり、このメモリアドレスは次のバイトコードが実行されるアドレスであり、現在のプログラムが実行する命令を記録する役割を果たします。

JVMクラスロードメカニズム

クラスのライフサイクルは次のとおりです。

 前の 5 つのステップは、クラスのロードと固定順序のプロセスでもあります。主に前の 5 つのステップを学習します。

具体的には、クラスロードとは、コンパイルされたクラスファイルである.classファイルをメモリ上にロードすることであり、クラスオブジェクトを取得する処理をクラスロードと呼びます。

プログラムを実行するには、命令とデータをメモリにロードする必要があります。これがクラスロードの動作です。

クラスロードの 5 つのステップは次のとおりです。

1.負荷

ここでのロード プロセスは実際には簡単です。つまり、.class ファイルを見つけて、そのファイルの内容を読み取るだけです。

しかし、.class ファイルを見つけるプロセスでは、親委任モデルという非常に重要なメカニズムが存在します。

保護者の委任モデル

JVM では、クラスをロードするには特別なモジュールのセット、つまりクラス ローダーが必要です。

JVM には、3 つの組み込みクラス ローダーがあります。

  • BootStrap ClassLoader は、Java 標準ライブラリのクラスをロードします。
  • Extension ClassLoader は、Sun/Oracle 拡張ライブラリであるいくつかの非標準クラスのロードを担当します。
  • アプリケーション ClassLoader は、プロジェクトで記述されたクラスとサードパーティ ライブラリのクラスをロードします。

特にクラスをロードするときのプロセスは次のようになります。

クラスの完全修飾クラス名を最初に指定する必要があり、クラス名「java.lang.String」は文字列の形式になります。

クラス ローダーがクラス ロード リクエストを受信した場合、最初にそれ自体でクラスをロードしようとするのではなく、リクエストを親クラス ローダーに委任して完了します。これはクラス ローダーの各レベルの場合であるため、すべてのロード リクエスト最終的には、最上位の BootStrap ClassLoader クラス ローダーに送信される必要があります。親ローダーがロード要求を完了できない (検索範囲内で必要なクラスが見つからない) と報告した場合にのみ、子ローダーはロードを試行します。あなた自身。

詳細については、次の図を参照してください。

2. 検証

.class ファイルは明確なデータ形式 (バイナリ) であるため、この段階の主な目的は、クラス ファイルのバイト ストリームに含まれる情報が「Java 仮想マシン仕様」のすべての制約に準拠していることを確認することです。

検証オプション

ファイル形式の検証

バイトコード検証

シンボリック参照の検証...

3. 準備する

準備段階は、クラスで定義された変数 (つまり、静的変数、静的によって変更された変数) にメモリを正式に割り当て、クラス変数の初期値を設定する段階です。

たとえば、次のコード:

public static int value = 123;

このとき、準備フェーズの value の値は 123 ではなく、0 になります。
 

4. 分析

解析フェーズは、Java 仮想マシンが定数プール内のシンボル参照を直接参照に置き換えるプロセス、つまり定数を初期化するプロセスです。

  • シンボリック参照: 文字列定数は .class ファイル内にすでに存在しますが、それらは相互の相対位置のみを認識し、メモリ内の特定の位置を認識しません。
  • 直接参照: 実際にメモリにロードされると、文字列定数はメモリ内の特定のアドレスに書き込まれます。このとき、文字列参照は直接参照(つまり、Java の共通参照)です。

5. 初期化

初期化フェーズでは、JVM がクラスに記述された Java コードを実際に実行し、アプリケーションに主導権を渡し、クラスの構築メソッドを実行するプロセスです。(クラスに親クラスがある場合は、最初に親クラスを初期化し、次にサブクラスを初期化する必要があります)。

トリガークラスのロード

注: クラスロードのアクションは、JVM が開始するとすぐにロードされることを意味するものではありません。これは、JVM 全体が遅延ロード戦略であるため、これは必要がなく、ロードされません。

次の 3 つの条件が読み込まれます。

  1. このクラスのインスタンスを作成しました
  2. このクラスの静的メソッド/静的プロパティが使用されます
  3. サブクラスを使用すると、親クラスのロードがトリガーされます

JVM ガベージ コレクション戦略 GC

Java のガベージ コレクションは、メモリを自動的に解放するのに役立つメカニズムです。

インタビューの質問: ガベージ コレクション メカニズムが必要な理由は何ですか?

プログラムの実行中、オペレーティング システムには大量のメモリ領域が適用されますが、これらの領域も使い果たされる可能性があります。メモリ領域はリサイクルされずに割り当てられ続けるため、掃除もせずに家庭内のゴミを作り続けるようなものです。 . .

上では JVM のいくつかの領域について説明しましたが、ガベージ コレクションはどの領域を解放するのでしょうか?

各スレッドにはスタックとプログラム カウンターのコピーがあることに注意してください。スレッドの消滅とともに消滅します。

メタデータ領域に格納されたクラス オブジェクトが破棄されることはほとんどありません。

したがって、解放されるのはヒープ内のスペースです。ヒープには主に新しいオブジェクトが格納されると上で説明しました。

GCはオブジェクト単位で解放されます。(リリースオブジェクト)

GC は主に 2 つのフェーズに分かれています。

1: 誰がゴミなのかを見つけ出す 

Javaではゴミオブジェクトかどうかを判断するために参照があり、参照がない場合はゴミと判断します。

1. 参照カウント

オブジェクト用に追加のスペースを配置し、そのオブジェクトを指す参照が複数あることを示す整数を保存します。Java は実際にはそのような方式を採用していません (Python と PHP はこの方式を採用しています)。

Test t1 = new Test();

 このとき、それを指す参照があるため、参照カウンタは 1 になります。

コードが次のようになった場合:

Test t1 = new Test();
Test t2 = t1;

 つまり、リファレンスが増加するとカウンタが増加し、リファレンスが破壊されるとカウンタが減少します。

カウンタが 0 の場合、オブジェクトには参照点がなく、ゴミであると見なされます。

しかし、欠点も明らかです。

  1. メモリ空間を無駄にしている
  2. 循環参照が存在します

2. アクセシビリティ分析 (このソリューションは Java で採用されています)。

オブジェクト間の参照関係をツリー構造として理解し、いくつかの特別な開始点から開始して、アクセスできる限り、それはガベージではなく到達可能であり、到達できないものをガベージとして扱います。

 このとき、ルートの参照を通じてツリー全体の任意のノードにアクセスできます。

到達可能性分析の重要な点は、上記の走査を実行するには開始点が必要であるということです。

開始点は次のとおりです。

  1. スタック上のローカル変数 (スタックごとの各ローカル変数が開始点です)
  2. 定数プールで参照されるオブジェクト
  3. メソッド領域の静的メンバーによって参照されるオブジェクト

一般に、アクセシビリティ分析は、すべての開始点から開始し、オブジェクト内のどの参照がそれらのオブジェクトにアクセスできるかを確認し、蔓をたどってすべてのアクセシブルなオブジェクトにアクセスし、トラバース中にオブジェクトを「到達可能」としてマークします。

参照カウントの 2 つの欠点を克服する到達可能性分析

しかし、それ自体の問題もあります。

  • 時間がかかるため、たとえオブジェクトがゴミになったとしても、スキャン処理に時間がかかるため、最初は見つけることができません。
  • 到達可能性解析を行う際にはつるをたどる必要があり、途中で現在のコード内のオブジェクトの参照関係が変化するとバグが発生する可能性があります。

したがって、このフォローアップ プロセスをより適切に完了するには、他のビジネス スレッドの作業を一時停止する必要があります。(STW)

(STW) ストップ・ザ・ワールド!

しかし結局のところ、Java は長年にわたって開発されており、リサイクルに引き込まれる際にも継続的に最適化されており、STW の問題にもより適切に対処できる可能性があります。

2: ガベージ オブジェクトを解放する

3 つの典型的な戦略

1: マークをクリアします

 今、次のようなスペースをメモリに適用すると、マークしたものはクリアする必要があるガベージ オブジェクトになります。

 この戦略は、ガベージ オブジェクトのメモリを直接解放することです。

しかし、この単純かつ粗雑な方法ではメモリの断片化が発生します。

メモリの断片化: アプリケーション空間は連続した空間ブロックですが、上図の空き領域は独立した空間に点在しています。現在、合計空き容量が1Gを超える可能性がありますが、500Mを申請したいのですが、申請できません。

2: コピーアルゴリズム

空間を2つの部分に分けるというアプローチです。一度に半分だけを使用してください。

コピーのアルゴリズムは、ゴミではないオブジェクトを片側にコピーし、その領域全体を均一に解放します。

 この時解放したいのは2と4で、残りの1と3を反対側にコピーする必要があります。それからここですべてを解放してください。

 コピー アルゴリズムはメモリの断片化の問題を解決しますが、次のような欠点もあります。

  • メモリ使用率が低い
  • ほとんどのオブジェクトが予約されており、ゴミがほとんどない場合、現時点ではコピーのコストが比較的高くなります。

3: マーク仕上げ

中間要素を削除する順序テーブルと同様に、処理のプロセスがあります。

 メモリの断片化の問題は解決されましたが、処理に伴う全体的なオーバーヘッドは比較的大きくなります。

JVM実装のアイデア

実際、JVM の実装は、上記の考え方を組み合わせた方法です。

世代を超えたリサイクルのアイデア

詳細:

  • オブジェクトには、そのオブジェクトがどれくらいの期間存在していたかを表すために年齢などの概念が設定されます。オブジェクトが生まれたばかりの場合、そのオブジェクトは 0 歳です。
  • スキャン (到達可能性分析) が実行されるたびに、ガベージ オブジェクトとしてマークされていない場合、オブジェクトの年齢は 1 年ずつ増加します。
  • このオブジェクトのアクティブ時間は年齢によって区別されます。

経験則: オブジェクトが古いほど、長持ちします。

年齢ごとに異なるリサイクル戦略を採用する

JVM は、これらの領域に対してさまざまな戦略を実装します。

1: 新しく作成したオブジェクトをエデンエリアに配置します

ガベージ コレクションが Eden 領域をスキャンした後、スキャンの最初のラウンドでほとんどのオブジェクトが GC によって削除されます。

2: エデン領域のオブジェクトが最初の GC で生き残った場合、そのオブジェクトはコピー アルゴリズムを通じて生存領域にコピーされます。

リビングエリアは(同じサイズの)2つの半分に分割されており、一度に使用されるのはそのうちの半分だけです。

GC がリビング エリアをスキャンし、ゴミ オブジェクトが見つかった場合は削除され、ゴミではない場合はコピー アルゴリズムによってリビング エリアの反対側にコピーされます。

3: オブジェクトが居住領域で数回の GC を生き延びると、年齢も古くなります。このとき、レプリケーション アルゴリズムを通じて古い世代にコピーされます。

4: 老年期に入った後は、年齢が比較的古いため、ゴミオブジェクトとしてマークされる概念も非常に小さいため、老年期の GC スキャンの頻度も減ります。

特殊なケース: オブジェクトが非常に大きい場合は、古い世代に直接入力します (大きいオブジェクトをコピーするコストは非常に高く、大きいオブジェクトはそれほど多くありません)。

おすすめ

転載: blog.csdn.net/qq_63525426/article/details/131725086