この記事では、スレッド共有からネイティブメソッドスタック、Javaヒープまで、JVMメモリモデルについて詳しく説明します。

この記事では、スレッド共有からネイティブメソッドスタック、Javaヒープまで、JVMメモリモデルについて詳しく説明します。

序文

JVM メモリ モデルを正式に学習する前に、次の点に注意してください。

JVM メモリ モデルと JAVA メモリ モデルは同じ概念ではありません。 JVM メモリ モデルは、ランタイム データ領域の構造の観点から説明される概念です。一方、JAVA メモリ モデルは、メイン メモリとスレッド プライベート メモリの観点から説明されています。次の 2 つの画像からわかるように:

[[285399]]

JAVA メモリ モデル

JVM メモリ モデル

  1. Java 仮想マシンは、クラス ローダー、サブシステム、ランタイム データ領域、実行エンジンという 3 つの主要モジュールで構成されています。この記事では、2 番目に大きなモジュールであるランタイム データ領域 (JVM メモリ モデル) について説明します。
  2. 実際、仮想マシンのこれらのモジュールは独立しているのではなく、相互に接続されています。 Java ファイルはクラス ファイルにコンパイルされ、クラス ローディング サブシステムを介してロードされ、情報は JVM によって管理されるメモリに流れ (一部の操作はローカル メモリと対話します)、その後ガベージ コレクションなどに送られます。これらはすべて一連の操作です。

概要

ランタイム データ領域は、いくつかの主要なモジュールに分かれています (上図を参照)。

スレッド共有領域:

  • JAVA ヒープ
  • 方法領域

スレッドプライベートエリア:

  • JAVAスタック
  • ネイティブメソッドスタック
  • プログラムカウンタ

この記事では、各地域を以下の視点から分析します。

  • 関数
  • 保存されたコンテンツ
  • メモリオーバーフローまたはメモリリークが発生していますか?
  • ガベージコレクションを実行するかどうか
  • 対応するガベージコレクションアルゴリズム
  • ガベージコレクションプロセス
  • パフォーマンスチューニング

スレッドプライベートエリア

プログラムカウンタ

プログラム カウンターはメモリ空間の小さな部分であり、現在のスレッドによって実行されたバイトコードの行番号インジケーターとして考えることができます。バイトコード インタープリターが動作しているとき、このカウンターの値を使用して、次に実行するバイトコード命令を選択します。分岐、ループ、ジャンプ、例外処理、スレッド回復はすべてこの領域に依存します。

簡単に言えば、この領域にはメソッド領域内のメソッド バイトコードへのポインタが格納され、次の命令、つまり実行される命令コードのアドレスを格納するために使用されます。

スレッドが Java メソッドを実行している場合、このカウンターは実行中の仮想マシン バイトコード命令のアドレスを記録します。ネイティブ メソッドが実行されている場合、カウンター値は空 (未定義) になります。

命令コード行が実行されると、JVM 実行エンジンはプログラム カウンターの値を更新します。

Java 仮想マシンのマルチスレッドは、スレッドを順番に切り替えてプロセッサ実行時間を割り当てることによって実装されるため、特定の瞬間にプロセッサ (マルチコア プロセッサの場合はコア) は 1 つのスレッド内の命令のみを実行します。そのため、スレッド切り替え後に正しい実行位置に戻すためには、各スレッドが独立したプログラム カウンターを持つ必要があります。スレッド間のカウンターは互いに影響を及ぼさず、独立して保存されます。このタイプのメモリ領域を「スレッド プライベート」メモリと呼びます。 (メソッドが呼び出され、そのメソッド内で別のメソッドが呼び出されます。これは、スタックの「先入れ先出し、後入れ後出し」モデルを正式に満たします)。

メモリ不足エラー: なし

仮想マシンスタック

これは Java メソッド実行のメモリ モデルを記述したもので、そのライフ サイクルはスレッドのライフ サイクルと同じです。

各メソッドは実行時にスタック フレームを作成します。各スタック フレームには、ローカル変数テーブル、オペランド スタック、動的リンク、メソッド終了などが含まれます。メソッドが呼び出され、そのメソッド内で別のメソッドが呼び出されます。これにより、スタックの「先入れ先出し、後入れ後出し」モデルが正式に満たされます。つまり、メソッドの呼び出しから実行までのプロセスは、スタック フレームをプッシュしてから仮想マシン スタックにポップするまでのプロセスに対応します。

上記は、深く理解するのが難しい、非常に機械的な概念のほんの一部です。以下では、例を使用して仮想マシン スタックのストレージ コンテンツを分析します。

まず簡単なプログラムを作成します。

  1. パッケージ com.sunwin.robotcloud.test;
  2. /**
  3. * 2019-11-01追梦1819によって作成されました。
  4. */
  5. パブリッククラスCalculateMain{
  6. 公共  int計算(){
  7. 整数a = 3;
  8. 整数b=4;
  9. 整数c = a+b;
  10. cを返します
  11. }
  12. 公共 静的void main(String[] args) {
  13. CalculateMain main = 新しい CalculateMain();
  14. int d = main.calculate();
  15. システム.out.println (d) ;
  16. }

上記のプログラムでは、スレッドが開始されると、仮想マシンはメインスレッド main に大きなメモリ空間を割り当て、次に main メソッドにスタック フレームを割り当てて、メソッドのローカル変数を格納します。

calculate() メソッドが実行されると、対応するメソッドのローカル変数を格納するために calculate() スタック フレームが割り当てられます。

メソッドはスタック フレームという単一のメモリ領域を割り当てることに注意することが重要です。

Java は高級言語であり、コードを通じてその実行プロセスを直接確認することは困難です。基礎となるバイトコードを解凍し、実行された命令コードを抽出することで、基礎となる実行プロセスを分析します。

CalculateMain.class ファイル ディレクトリに入り、次のコマンドを実行します。

コマンド コードを CalculateMain.txt ファイルに直接出力します。

  1. 編集  「CalculateMain.java」  
  2. パブリッククラス com.sunwin.robotcloud.test.CalculateMain {
  3. パブリックcom.sunwin.robotcloud.test.CalculateMain();
  4. コード:
  5. 0: ロード_0
  6. 1:invokespecial #1 // メソッド java/lang/Object。 「<init>」 :()V
  7. 4:戻る 
  8.  
  9. 公共 整数計算();
  10. コード:
  11. 0: アイコンst_3
  12. 1: istore_1
  13. 2: アイコンst_4
  14. 3: istore_2
  15. 4: ロード1
  16. 5: ロード2
  17. 6: 追加
  18. 7: istore_3
  19. 8: ロード3
  20. 9: 戻る
  21.  
  22. 公共 静的void main(java.lang.String[]);
  23. コード:
  24. 0: 新しい #2 // クラス com/sunwin/robotcloud/test/CalculateMain
  25. 3: 重複
  26. 4: 呼び出しスペシャル #3 // メソッド"<init>" :()V
  27. 7: アストア1
  28. 8: ロード1
  29. 9:invokevirtual #4 // メソッド calculate:()I
  30. 12: istore_2
  31. 13: getstatic #5 // フィールド java/lang/System.出力:Ljava/io/PrintStream;
  32. 16: ロード2
  33. 17:invokevirtual #6 // メソッド java/io/PrintStream.println:(I)V
  34. 20:戻る 
  35. }

まず、calculate() メソッドを見てみましょう。上記の手順に従って、JVM の取扱説明書を照会し、上記のプログラムの実行フローを取得します。

0. int 型定数 3 を (オペランド) スタックにプッシュします。

1. int 型の値 3 をローカル変数 1 (1 は配列の添え字) に格納します。つまり、ローカル変数テーブル内の a にメモリの一部を割り当てます (3 を格納するため)。

2. int型定数4を(オペランド)スタックにプッシュします。

3. int型の値4をローカル変数2に格納します。

4. ローカル変数 1 から int 型の値をロードします。つまり、ローカル変数テーブルの値 3 を取り出してオペランド スタックにロードします。

5. ローカル変数 2 から int 型の値を読み込みます。

6. 2つの値を追加します。

7. (オペランド スタックに数値を格納しますか?) int 値 7 をローカル変数 3 に格納します。

8. ローカル変数 3 から int 型の値を読み込みます。

9. 計算された値を返します。

上記はメソッド実行時のメモリ内のローカル変数のフロー処理です。要約すると、次のとおりです。

オペランド スタックは、操作中のデータの一時的な転送ステーションに相当します。

ローカル変数テーブル: ローカル変数の保存スペース。ワード長を単位とし、0からカウントされる配列です。int、float、reference、retrueAddress型の値は1つの項目のみを占めます。 byte、short、char 型の値は、配列に格納される前に int 値に変換されます。 long 型と double 型の値は、連続する 2 つの項目を占めます。インデックスは最初の値を指します。

ただし、仮想マシンは byte、short、char を直接サポートしますが、ローカル変数テーブルとオペランド スタックでは int 値に変換され、ヒープとメソッド領域では元の型のままであることに注意してください。

オペランド スタック: データ操作のための一時スペース。ローカル変数テーブルに似ています。唯一の違いは、インデックスではなく、プッシュとポップによってアクセスされることです。

動的リンク: 実行時に動的に生成されるメソッドの JVM 命令コードのメモリ アドレスを格納します。

オブジェクトにはオブジェクト ヘッダーがあり、その中で型ポインターはメソッド領域内のクラス メタ情報を指します。

メソッド終了: メソッドを終了して次のメソッドに入るプログラム カウンターの値を格納します。

JAVAスタック構造

例外: スレッドによって要求されたスタックの深さが仮想マシンで許可されている深さより大きい場合、StackOverflowError 例外がスローされます。仮想マシン スタックを動的に拡張できる場合 (現在の Java 仮想マシンのほとんどは動的に拡張できますが、Java 仮想マシン仕様では固定長の仮想マシン スタックも許可されています)、拡張中に十分なメモリを要求できない場合は OutOfMemoryError 例外がスローされます。

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

ネイティブ メソッド スタックは、実際には Java 仮想マシン スタックと非常によく似ています。唯一の違いは、Java 仮想マシン スタックは Java メソッドを提供し、ネイティブ メソッド スタックはネイティブ メソッドを提供することです。仮想マシン仕様では、ネイティブ メソッド スタック内のメソッドで使用される言語、使用法、データ構造に関する必須規定がないため、特定の仮想マシンが自由に実装できます。

StackOverflowError および OutOfMemoryError 例外もスローされる可能性があります。

スレッド共有領域

方法領域

この領域には、仮想マシンによってロードされたクラス情報 (フィールド メソッドのバイトコード、一部のメソッドのコンストラクタ)、定数、静的変数、コンパイルされたコード情報など、およびクラスのすべてのフィールドとメソッドのバイトコードが格納されます。コンストラクターなどのいくつかの特殊なメソッドだけでなく、インターフェースのコードもここで定義されます。つまり、定義されたすべてのメソッドの情報がこの領域に保存されます。静的変数 + 定数 + クラス情報 (コンストラクター/インターフェース定義) + ランタイム定数プールがすべて存在します。

不連続、固定サイズ、拡張可能、またはガベージ コレクターがない場合もあります。この領域ではガベージ コレクションが存在します。ただし、頻度は低くなります。

メソッド領域は定義と概念であり、永続的な世代またはメタスペースは実装メカニズムです。

OutOfMemoryError: はい

ランタイム定数プール

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

OutOfMemoryError: はい

JAVA ヒープ

ヒープは、Java 仮想マシンによって管理されるメモリの最大の領域であり、その唯一の機能はオブジェクト インスタンスを格納することです。ほぼすべてのオブジェクト (定数プールを含む) はヒープ上にメモリを割り当てます。

インスタンスの割り当てを完了するためのメモリがヒープ内に存在せず、ヒープをそれ以上拡張できない場合は、OutOfMemoryError 例外がスローされます。

ガベージコレクターの主な管理領域。

ガベージコレクションの観点から、この領域は新しい世代と古い世代に分けられます。新世代はエデン空間とサバイバー空間に分かれています。サバイバー ペースは、サバイバー From スペースとサバイバー To スペースに分かれています。次の図に示すように:

上記のエリアのサイズ分布は次のとおりです。

若い世代: ヒープ全体の1/3

古い世代: ヒープの 2/3

エデン地区:新世代の8/10

ゾーンからの生存者: 新世代の1/10

生存者ゾーン: 新世代の1/10

メモリ割り当ての観点から言えば、複数のスレッド専用の割り当てバッファを分割することができます。

ヒープ スペースの場合、本質はオブジェクト インスタンスを格納することです。ただし、パーティショニングは、オブジェクト インスタンスの割り当てと管理を改善するためだけに行われます。ヒープ空間内のオブジェクト インスタンスの管理とリサイクルについては、次の章で説明します。

同時に、物理的には連続していないかもしれませんが、論理的には連続している必要があります。

JVM メモリ モデルの全体的な構造は次のとおりです。

オブジェクトのリサイクルプロセス

以下の写真はインターネットから取得したものです。

すべてのクラスはエデンエリアに新しく作成されます。 Eden 領域がいっぱいになると、マイナー GC がトリガーされ、他のオブジェクトから参照されなくなったオブジェクトは破棄され、残りのオブジェクトは From Survivor 領域に移動されます。マイナー GC がトリガーされるたびに、オブジェクトの世代年齢が +1 されます (世代年齢はオブジェクト ヘッダーに保存されます)。 From Survivor 領域がいっぱいになると、From Survivor 領域でマイナー GC がトリガーされ、未収集オブジェクトの世代年齢が 1 ずつ増加し続け、To Survivor 領域に移動されます。このとき、エデン内の未収集オブジェクトも「To Survivor」エリアに移動されます。 「To Survivor」エリアがいっぱいになると、「From Survivor」エリアに移動されます。

オブジェクトの世代年齢が 15 に達すると、オブジェクトは古い世代に入ります (静的変数 (オブジェクト型)、データベース接続プールなど)。古い世代もいっぱいの場合は、この時点で古い領域のメモリをクリーンアップするためにメジャー GC (フル GC) が発生します。旧領域でFull GCを実行し、オブジェクトを保存できないことが判明した場合、OOM例外OutOfMemoryErrorが生成されます。

予防

  1. 異なるバージョンでは、ランタイム データ領域に次のようなわずかな違いがあります。メタデータ領域: メタデータ領域は、永続世代 (jdk1.8 より前) を置き換えます。本質的には永久世代と同様です。どちらも、JVM 仕様のメソッド領域の実装です。違いは、メタデータ領域が仮想マシン内になく、ローカルの物理メモリを使用することです。永続的な世代は仮想マシン内にあります。永続世代は、論理構造上はヒープに属しますが、物理的には属しません。ヒープ サイズ = 新しい世代 + 古い世代。メタデータ領域でも OutOfMemory 例外が発生する可能性があります。 jdk1.6 以前: 永続的な世代があり、定数プールはメソッド領域にあります。 jdk1.7: 永続世代がありますが、徐々に「永続世代に移行」し、定数プールはヒープ内にあります。 jdk1.8 以降: 永続的な世代はなく、定数プールはメタスペースにあります (仮想マシンによって管理されるメモリではなく、コンピューターの直接メモリを使用します)。
  2. JDK 1.8 で永続世代がメタデータ領域に置き換えられたのはなぜですか?公式の説明: 永続的な世代を削除するのは、JRockit には永続的な世代がなく、構成する必要がないため、HotSpot JVM と JRockit VM を統合するための取り組みです。 (簡単に言うと、二人が競争して、勝った方が聴く権利を得るのです。)
  3. メタデータ領域の動的な拡張。デフォルトの –XX:MetaspaceSize 値は、最高水準点の 21 MB です。一度触れると、Full GC がトリガーされ、役に立たないクラスがアンロードされ (クラスに対応するクラス ローダーが動作しなくなります)、最高水準点がリセットされます。新しい最高水準値は、GC 後に解放されたメタスペースによって異なります。解放されるスペースが少ないと、最高水準点が上昇します。解放されたスペースが多すぎると、最高水準点が下がります。

<<:  テクノロジーの選択: Docker コンテナ エンジン

>>:  ブロックチェーン分散ストレージの利点は何ですか?知っておくべき4つの大きなメリット

推薦する

ウェブサイトを「盲目的に宣伝する」SEO手法について、どれくらいご存知ですか?

多くの場合、数か月間独自に外部リンクを投稿し、コンテンツを作成しているのに、Web サイトがまったく...

SEO の奇跡を見るために、ウェブマスターはどれだけの忍耐力を持っているのでしょうか?

私は1か月以上SEOを勉強しています。私には学習と練習ができる独自のウェブサイトもあります。オリジナ...

ゲームの王様: テンセント帝国のハーフライフ

それは「始まり」の終わりなのか、それとも「終わり」の始まりなのか?中国ゲーム業界の「Half-Lif...

最近、Baidu は大きく変化しました。それはある要因によるものだと言われています。

百度は最近大きな変化を遂げ、国民の間にパニックを引き起こしている。まず、Baidu News の検索...

クラウドプロバイダーを選ぶ際に考慮すべき8つの重要なポイント

数年前、業界関係者は「IBM のサービスを選択しても解雇されることはない」と言っていました。今日の競...

セルフサービス Web サイト構築システムに参加することを選択するときに注意すべき点は何ですか?

2018年最もホットなプロジェクト:テレマーケティングロボットがあなたの参加を待っています多くの中小...

中小企業がネットワークマーケティングを行うための逆転の発想法

世界はますます小さくなり、情報の伝達速度はますます速くなっています。人々がコミュニケーションのために...

例の説明: 両者間の ZS 論争の焦点は「ユーザー エクスペリエンス」です。

まず、私の経歴についてお話しします。大した経歴ではありません。私は SEO 実践者であり、ローカル ...

3 つの事例から、データ ウェアハウスのデータ フローを構築する方法を学びます。

翻訳者 |張峰企画 |趙雲データフロー、分析、その他のソフトウェア開発など、プロジェクトごとに課題が...

Microsoft Dynamics 365が正式に中国に上陸、Microsoftのインテリジェントクラウド「3つのクラウド」が融合し、「2つの5」クラウド戦略を開始

2019年4月23日、中国市場で商用運用する国際パブリッククラウドである21Vianetが運営するM...

セキュアサーバー - $88/E3-1230V2/8g メモリ/500g ハードディスク/15T トラフィック/5IP/フェニックス シティ

secureservers のサーバーの割引コード 20OFF1230 を見つけました。元の価格は月...

見逃せない人気の継続的インテグレーションツール 8 選

「継続的インテグレーション」に精通している方であれば、「継続的インテグレーションの使用は必須となって...

Alibaba Cloud、データベースパフォーマンスの最適化と問題診断の問題を解決するCloudDBAを発表

トラブルシューティングとパフォーマンス チューニングは、データベース分野では常に専門的な問題であり、...

平均サーバー - $7/KVM/Win/1g メモリ/35g ハードディスク/2T トラフィック

以前は、meanservers についてあまり知りませんでしたが、今年設立された VPS ベンダーで...

Webmaster.com からの毎日のレポート: JD.com の交換の抜け穴が見つかり、Google が「.中国」を登録

1. JDチャージプラットフォームに抜け穴があり、2億元を失ったという噂昨日22時30分、JD.co...