blog

JVMのメモリー構造 - 2万語の長い記事で教え、学ぶ!

メモリは非常に重要なシステムリソースであり、ハードディスクとCPUの間の中間倉庫とブリッジであり、オペレーティングシステムとアプリケーションのリアルタイム動作を運びます。JVMのメモリレイアウトは、メ...

Dec 24, 2020 · 36 min. read
シェア
  • Java 8におけるメモリ生成の改善
  • スタック・オーバーフローの例?
  • スタックをオーバーフローから救うために、スタックのサイズを変更できますか?
  • より多くのスタック・メモリを割り当てる方が良いのですか?
  • ゴミ収集は仮想マシンのスタックに関係しますか?
  • メソッドで定義されたローカル変数はスレッドセーフですか?
  • ランタイム・データ領域

    メモリは非常に重要なシステム資源であり、ハードディスクとCPUの間の中間倉庫とブリッジであり、オペレーティングシステムとアプリケーションプログラムのリアルタイムオペレーションを運びます。JVMのメモリレイアウトは、Javaの実行プロセス中のメモリ申請、割り当て、管理の戦略を指定し、JVMの効率的で安定したオペレーションを保証します。JVMのメモリレイアウトは、Javaの実行プロセス中のメモリ要求、割り当て、および管理の戦略を指定し、JVMの効率的で安定した動作を保証します。

    次の図は、JVMの全体的なアーキテクチャを示しており、中央にはJava仮想マシンによって定義されたさまざまな実行時データ領域があります。

    Java VMは、アプリケーションの実行時に使用される多くの実行時データ領域を定義しており、その一部はVMの起動時に作成され、VMの終了時に破棄されます。その他はスレッド固有で、これらのスレッド固有データ領域はスレッドの開始と終了に伴って作成と破棄が行われます。

    • スレッド・プライベート:プログラム・カウンタ、スタック、ローカル・スタック
    • スレッド共有:ヒープ、オフヒープ・メモリ

    これらのメモリ領域をそれぞれ無害化する方法を、簡単なものから順に説明します。

    プログラム・カウンター

    プログラムカウンタレジスタ(Program Counter Register)、レジスタの名前の由来はCPUのレジスタ、命令関連のスレッド情報を格納するレジスタで、CPUはレジスタにデータをロードすることでしか動作できません。

    ここでは、広義の物理レジスタではなく、プログラム・カウンタであり、その方が適切であり、不要な誤解を招きにくい。JVMのPCレジスタは、物理的なPCレジスタの抽象的なシミュレーションです。

    プログラム・カウンタは、現在のスレッドで実行されているバイト・コードの行番号インジケータと考えることができるメモリの小片です。

    役割

    PC レジスタは、次の命令を指すアドレス、これから実行される命令のコードを格納するために使用されます。次の命令は実行エンジンによって読み取られます。

    現在のクラスに対応するコード・エリア、ローカル変数テーブル、例外テーブル、コード・ライン・オフセット・マッピング・テーブル、定数プール、その他の情報を見ることができます)。

    概要

    • コード・エリアは非常に小さなメモリ・スペースで、ほとんど無視できます。また、最も高速に実行される記憶領域でもあります
    • JVMの仕様では、各スレッドは、スレッドプライベートであり、スレッドと同じライフサイクルを持つ独自のプログラムカウンタを持っています。
    • スレッドによって実行されるメソッドは常に1つだけで、これは現在のメソッドと呼ばれます。プログラムカウンタは、現在のスレッドがJavaメソッドを実行している場合はJVMバイトコード命令のアドレスを記録し、naticeメソッドを実行している場合は未指定の値を記録します。
    • 分岐、ループ、ジャンプ、例外処理、スレッド再開などの基本的な機能は、すべてこのカウンタに依存しています。
    • バイトコードインタープリタは、このカウンタの値を変更して、次に実行されるバイトコード命令を選択します。
    • これは、JVM仕様の中で、OutOfMemoryError条件を提供しない唯一の領域です。

    : バイトコード命令のアドレスを格納するためにPCレジスタを使用する目的は何ですか?現在のスレッド実行のアドレスを記録するために PC レジスタを使用するのはなぜですか?

    :CPUはスレッドを切り替え続ける必要があり、切り替わったときに、どこから実行を開始するかを知る必要があるからです。JVMのバイトコードインタープリタは、次に実行すべきバイトコード命令の種類を指定するために、PCレジスタの値を変更する必要があります。

    :なぜPCレジスタはスレッド・プライベートに設定されているのですか?

    :複数のスレッドが特定の時間帯にスレッドメソッドの1つだけを実行することになり、CPUはタスクスイッチングを繰り返し、必然的に割り込みや再開が頻発することになります。各スレッドが現在実行しているバイトコード命令アドレスを正確に記録できるようにするため、PCレジスタを各スレッドに割り当て、各スレッドは互いに影響を与えずに独立して計算されます。

    次に、仮想マシンのスタック

    概要

    Java仮想マシン・スタックは、初期にはJavaスタックとも呼ばれていました。各スレッドは、それが作成されるときに仮想マシン・スタックを作成します。仮想マシン・スタックは、単一のJavaメソッド呼び出しに対応する単一のスタック・フレームを保持し、スレッドプライベートで、スレッドと同じライフサイクルを持ちます。

    役割:Javaプログラムの動作を監督し、メソッドのローカル変数、結果の一部を保存し、メソッドの呼び出しとリターンに参加します。

    特徴

    • スタックは、ストレージを割り当てるための高速かつ効率的な方法であり、アクセス速度のプログラムカウンタに次ぐ第二
    • 仮想マシンスタックのJVMの直接操作のみ2:各メソッドの実行は、スタックを伴ってスタックのうち、メソッドの実行の終了
    • スタックは、ゴミ収集の問題がありません

    スタック内の可能な例外:

    Java仮想マシンの仕様は 、Java仮想マシンのスタックのサイズを動的または固定のいずれかにすることができます

    • 固定サイズの Java 仮想マシン・スタックでは、各スレッドの Java 仮想マシン・スタックのサイズは、スレッドが作成されるときに独立して選択できます。スレッドがJava仮想マシン・スタックで許可される最大値よりも大きなスタック・サイズを要求すると、Java VMは StackOverflowError 例外をスローします。
    • Java 仮想マシン・スタックが動的に拡張可能で、拡張しようとしたときに十分なメモリーを要求しなかったり、新しいスレッドを作成するときに対応する仮想マシン・スタックを作成するのに十分なメモリーがなかったりすると、Java 仮想マシンはOutOfMemoryError例外をスローします。

    スレッドの最大スタック・スペースは、-Xss パラメータで設定できます。 スタックのサイズは、関数呼び出しの到達可能な最大深度を直接決定します。

    スタック格納単位

    スタックには何が格納されますか?

    • 各スレッドは独自のスタックを持ち、スタック上のデータはスタックフレームの形をしています。
    • スレッド上で実行される各メソッドには、対応するスタックフレームがあります。
    • スタックフレームはメモリのブロックであり、メソッドが実行されるときにメソッド内のデータに関する情報を保持するデータセットです。

    スタックの仕組み

    • JVMがJavaスタックで直接行う操作は、FIFO/LIFOの原則に従って、スタックへのフレームの押忍スタックからのフレームの退避の2つだけです。

    • アクティブなスレッドでは、ある時点でアクティブなスタック・フレームは1つだけです。つまり、現在実行中のメソッドのスタックフレームのみが有効であり、このスタックフレームはカレントスタックフレームと呼ばれ、カレントスタックフレームに対応するメソッドはカレントメソッドであり、このメソッドを定義するクラスはカレントクラスです。

    • 実行エンジンによって実行されるすべてのバイトコード命令は、現在のスタックフレーム上でのみ動作します。

    • このメソッド内で別のメソッドが呼び出されると、新しいスタックフレームが作成され、新しいカレントスタックフレームと呼ばれるスタックの先頭に置かれます。

    • つまり、スタックフレーム内で他のスレッドのスタックフレームを参照することはできません。

    • 現在のメソッドが別のメソッドを呼び出す場合、メソッドが戻ると、現在のスタックフレームはメソッドの実行結果を前のスタックフレームに戻します。

    • Javaのメソッドには関数に戻る2つの方法があり、1つはreturn命令を使った通常の関数リターン、もう1つは例外を投げる方法で、どちらにしてもスタックフレームが排出されます

    IDEAがデバッグしているとき、デバッグウィンドウでFramesの様々なメソッドがどのようにスタックされていくかを見ることができます。

    スタックフレームの内部構造

    各スタックフレームには

    • ローカル変数のテーブル
    • オペランド・スタック
    • ダイナミック・リンク:実行時定数プールへのメソッド参照
    • メソッドのリターンアドレス:メソッドが正常または異常終了するアドレス
    • いくつかの追加情報

    スタックフレームの5つの部分について、さらに深く掘り下げます。

    ローカル変数テーブル

    • ローカル変数テーブル(ローカル変数配列またはローカル変数テーブルとも呼ばれます。
    • は、メソッド・パラメータとメソッド内で定義されたローカル変数を格納するために使用される変数値ストアのセットで、コンパイラが知っているさまざまなJava仮想マシン・ベース・データ型オブジェクト参照returnAddress 型を含みます。
    • ローカル変数テーブルはスレッドのスタック上に構築されるため、スレッドのプライベート・データであり、データ・セキュリティの問題はありません
    • maximum local variables ローカル変数テーブルの必要なサイズはコンパイル時に決定され、メソッドの Code プロパティのデータ項目に格納されます。ローカル変数テーブルのサイズは、メソッドの実行中には変更されません。
    • ネストされたメソッド呼び出しの数は、スタックのサイズによって決まります。一般に、スタックが大きいほど、ネストされたメソッド呼び出しが多くなります。関数の場合、引数やローカル変数が多ければ多いほど、ローカル変数テーブルが肥大化し、メソッド呼び出しに渡す必要がある情報が増えるため、スタックフレームが大きくなります。その結果、関数呼び出しのスタック領域が大きくなり、ネストされた呼び出しが少なくなります。
    • ローカル変数テーブルの変数は、現在のメソッド呼び出しに対してのみ有効です。メソッドの実行中、仮想マシンはローカル変数テーブルを使用してパラメータ変数リストにパラメータ値を渡す処理を完了します。メソッド呼び出しが終了すると、ローカル変数テーブルはメソッド・スタック・フレームと共に破棄されます。
    • パラメータ値は常に、インデックス 0 で始まり、配列長のインデックス -1 で終わるローカル変数配列に格納されます。
    Slot
    • ローカル変数テーブルの最も基本的な記憶単位はスロットです。

    • ローカル変数テーブルでは、32ビットまでの型は1つのスロットを占め(returnAddress型を含む)、64ビット型は連続した2つのスロットを占めます。

      • longとdoubleは2つのスロットを占有します。
    • JVMは、ローカル変数テーブルの各スロットにアクセスインデックスを割り当てます。このアクセスインデックスを通じて、ローカル変数テーブルで指定されたローカル変数値に正常にアクセスすることができ、インデックス値は0からローカル変数テーブルのスロットの最大数までの範囲です。

    • インスタンスメソッドが呼び出されると、メソッド本体内で定義されたメソッドパラメータとローカル変数が ローカル変数ティブールの各スロットに順番にコピーされます。

    • ローカル変数テーブルの64ビットのローカル変数の値にアクセスする必要がある場合は、前のインデックスを使用するだけです

    • 現在のフレームがコンストラクタまたはインスタンスメソッドによって作成された場合、オブジェクト参照thisはスロットインデックス0に格納され、残りのパラメータはパラメータリストの順番に続きます。

    • スタックフレームのローカル変数テーブルのスロットは再利用可能です。 ローカル変数がスコープ外になった場合、そのスコープ後に宣言された新しいローカル変数は、スコープ外のローカル変数のスロットを再利用する可能性が高く、リソースを節約できます。

    • スタック・フレームにおいて、パフォーマンス・チューニングに最も密接に関係するのはローカル変数テーブルです。メソッドの実行中、VM はローカル変数テーブルを使用してメソッドのパスを完了します。
    • ローカル変数テーブルで直接または間接的に参照されるオブジェクトがリサイクルされない限り、ローカル変数テーブルの変数も重要なゴミ収集ルートノードです。

    オペランドスタック

    • 個々のスタックフレームには、ローカル変数テーブルに加えて、式スタックとも呼ばれるLIFOオペランドスタックがあります。

    • オペランド・スタックは、メソッドの実行中、バイトコード命令に従って、オペランド・スタックにデータを書き込んだり、オペランド・スタックからデータを取り出したりします。

    • 特定のバイトコード命令は値をオペランドスタックに押し込み、残りのバイトコード命令はオペランドをスタックから取り出します。そして、それらを使用した後、結果がスタックに押し込まれます。例えば、コピー、スワップ、合計のような操作を実行します。

    概要
    • オペランド・スタックは、JVM 実行エンジンのワークスペースです。 メソッドが実行を開始すると、新しいスタック・フレームが作成され、メソッドのオペランド・スタックは空になります
    • 各オペランド・スタックには、値を格納するための明示的なスタック深度があります。 必要な最大深度はコンパイル時に定義され、メソッドの Code プロパティの max_stack データ項目に格納されます。
    • スタックのどの要素も、Java のどのデータ型でもかまいません。
      • 32 ビット型は 1 スタック・ユニットの深さを占めます。
      • 64 ビット・タイプは 2 つのスタック・ユニットの深さを占めます。
    • オペランド・スタックは、データ・アクセスのためのインデックス付けはされませんが、標準的なスタック・イン操作とスタック・アウト操作によって一度だけアクセスできます。
    • メソッドが戻り値で呼び出された場合、戻り値は現在のフレームのオペランドスタックに押し込まれ、PC レジスタは次に実行されるバイトコード命令で更新されます。
    • オペランドスタック内の要素のデータ型は、バイトコード命令のシーケンスと厳密に一致しなければなりません。このことは、コンパイラがコンパイル時に検証し、クラス読み込みプロセスのクラス検証フェーズのデータフロー解析フェーズでも再度検証されます。
    • また、Java仮想マシンの解釈エンジンはスタックベースの実行エンジンであると言われており、スタックとはオペランドスタック
    トップ・オブ・スタック・キャッシュ

    HotSpotの実行エンジンは非レジスタベースのアーキテクチャを使用していますが、HotSpot VMの実装がレジスタリソースを間接的に使用しないという意味ではありません。レジスタは物理的なCPUの不可欠な部分であり、CPU内の重要な高速ストレージリソースでもあります。一般的に、レジスタの読み書き速度は非常に速く、メモリの読み書き速度の数十倍もありますが、レジスタリソースは非常に限られており、プラットフォームによってCPUレジスタの数は異なり、不規則です。レジスタは主に、ローカルマシン命令や値、次に実行される命令のアドレスなどのデータをキャッシュするために使用されます。

    スタック・ベースのVMで使用されるゼロ・アドレス命令はよりコンパクトですが、演算の完了には必然的に多くの入出力命令を使用する必要があり、これはより多くの命令ディスパッチとメモリ・リード/ライトが必要になることも意味します。オペランドはメモリに格納されるため、頻繁なメモリリード/ライトは実行速度に影響します。この問題を解決するために、HotSpot JVMの設計者は、物理CPUレジスタ内のすべてのトップオブスタック要素をキャッシュするトップオブスタックキャッシング技術を提案しました。

    ダイナミック・リンク

    • 各スタック・フレームには、そのスタック・フレームが属するメソッドへの参照が実行時定数プー ルに含まれています。この参照は、現在のメソッドをサポートするコードを動的にリンクできるように含まれています。
    • Java ソース・ファイルがバイトコード・ファイルにコンパイルされると、すべての変数とメソッドへの参照が定数としてクラス・ファイルの定数プールに格納されます。例えば、別のメソッドを呼び出すメソッドを記述する場合、定数プール内のメソッドへのシンボリック参照によって示され、ダイナミック・リンクの役割は、これらのシンボリック参照を呼び出しメソッドへの直接参照に変換することです
    JVMがメソッド呼び出しを実行する方法

    メソッドの実行とは異なり、メソッド呼び出しフェーズの唯一のタスクは、呼び出されるメソッドのバージョンを決定することであり、当面はメソッドの内部操作には関与しません。Classファイルのコンパイル・プロセスには、従来のコンパイラのリンク・ステップは含まれておらず、すべてのメソッド呼び出しは、実際のランタイム・メモリ・レイアウト内のメソッドのエントリ・アドレス(直接参照)ではなく、Classファイル内のシンボリック参照として格納されます。直接参照)。つまり、ターゲット・メソッドへの直接参照は、クラスのロード段階、あるいは実行時にしか決定できません。

    コンテンツのこの部分は、メソッドの呼び出しに加えて、しかしまた、解決、割り当て、ここで紹介されていない含まれていますし、掘る!

    JVMでは、呼び出しメソッドへの直接参照へのシンボリック参照の変換は、メソッドのバインディングメカニズムに関連しています。

    • 静的リンク:バイトコード・ファイルがJVMにロードされるとき、呼び出されるターゲット・メソッドがコンパイル時にわかっていて、実行時に変更されない場合。この場合、シンボリック参照を呼び出しメソッドの直接参照に変換するプロセスを静的リンクと呼びます。
    • ダイナミック・リンク:呼び出されるメソッドがコンパイル時に決定できない場合、つまり、呼び出されるメソッドのシンボリック参照がプログラムの実行時にのみ直接参照に変換できる場合、この参照変換プロセスは動的な性質を持つため、ダイナミック・リンクとも呼ばれます。

    メソッドに対応するバインディングメカニズムは、アーリーバインディングとレイトバインディングです。バインディングは、フィールド、メソッド、クラスへのシンボリック参照が直接参照に置き換えられるプロセスです

    • アーリーバインディング:アーリーバインディングとは、呼び出されるターゲットメソッドがコンパイル時にわかっていて、実行時に変更されない場合、そのメソッドをそのメソッドが属する型にバインドできるこの意味します。 この方法では、呼び出されるターゲットメソッドが明確なので、静的リンクを使用してシンボリック参照を直接参照に変換することができます。
    • 後期バインディング:呼び出されるメソッドをコンパイラが決定できず、メソッドを実際の型に従って実行時にのみバインドできる場合、このタイプのバインディングは後期バインディングと呼ばれます。
    仮想メソッドと非仮想メソッド
    • メソッドがコンパイラで決定された特定の呼び出しバージョンを持つ場合、このバージョンは実行時に不変です。例えば、staticメソッド、privateメソッド、finalメソッド、インスタンスコンストラクタ、parentメソッドは非仮想メソッドです。
    • その他のメソッドは仮想メソッドと呼ばれます。
    仮想メソッドの表

    オブジェクト指向プログラミングでは、動的代入が頻繁に使用されますが、動的代入のたびにクラスメソッドメタデータから適切なターゲットを再度検索しなければならないとすると、実行効率に影響を与える可能性があります。パフォーマンスを向上させるために、JVMはクラスのメソッド領域に仮想メソッドテーブルを使用し、ルックアップの代わりにインデックス化されたテーブルを使用します。非仮想メソッドはテーブルに表示されません。

    各クラスには、各メソッドの実際のエントリーを保持する仮想メソッド・テーブルがあります。

    仮想メソッド・テーブルはクラス・ロードの結合フェーズで作成され、初期化されます。クラス変数の初期値が準備できると、JVMはそのクラスのメソッド・テーブルも初期化します。

    メソッドのリターンアドレス

    メソッドが呼び出された PC レジスタの値を格納するために使用されます。

    メソッドの終了方法は次の 2 つです。

    • 通常
    • 未処理の例外による異常終了

    どちらの方法で終了しても、メソッドが終了した後はメソッドが呼び出された場所に戻ります。メソッドが正常に終了した場合、呼び出し元のPCカウンタの値がリターンアドレス、つまりメソッドを呼び出した命令の次の命令のアドレスとして使用されます。一方、例外によって終了する場合、リターン・アドレスは例外テーブルによって決定され、この情報は通常スタック・フレームには格納されません。

    メソッドの実行が開始されると、メソッドを終了する方法は2つしかありません:

    1. 実行エンジンが任意のメソッドによって返されたバイトコード命令に遭遇し、戻り値が上位のメソッド呼び出し元に渡される江東です。

      メソッドの通常の呼び出しが完了した後にどの戻り値命令を使用するかは、メソッドの戻り値の実際のデータ型に依存します。

      バイトコード命令では、ireturn(戻り値のデータ型が boolean、byte、char、short、int の場合に使用)、lreturn、freturn、dreturn、areturn のほか、void と宣言されたメソッド、インスタンスの初期化メソッド、クラスやインタフェースの初期化メソッドに対する return 命令があります。インターフェイスの初期化メソッド。

    2. メソッドの実行中に例外が発生し、その例外がメソッド内で処理されなかった場合、つまり、メソッドの例外テーブルで一致する例外ハンドラが検索されなかった場合、メソッドは終了します。例外の終了省略最後に

      メソッド実行中に例外がスローされた場合の例外処理は例外処理テーブルに格納され、例外が発生したときに例外を処理するコードを簡単に見つけることができます。

    基本的に、メソッドの終了は、現在のスタックフレームがスタックから抜ける処理です。この時点で、上位メソッドのローカル変数テーブル、オペランドスタックを復元し、呼び出し元のスタックフレームのオペランドスタックに戻り値を押し込み、PCレジスタ値を設定するなどして、呼び出し元のメソッドが実行を継続できるようにする必要があります。

    通常の完了終了と例外完了終了の違いは、例外完了終了による終了は、上位の呼び出し元に対して戻り値を生成しないことです。

    追加情報

    スタックフレームは、Java 仮想マシン実装に関連する追加情報を運ぶことが許されています。例えば、アプリケーションのデバッグをサポートする情報ですが、この情報は特定の仮想マシン実装に依存します。

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

    ネイティブ・メソッド・インターフェース

    簡単に言うと、ネイティブ・メソッドはJavaがJava以外のコードを呼び出すためのインターフェースです。Unsafe クラスには多くのネイティブ・メソッドがあります。

    なぜネイティブ・メソッドを使用するのですか?

    Javaは非常に使いやすいのですが、Javaで実装するのが容易でないレベルのタスクがあったり、プログラムの効率が重要だったりする場合、次のような疑問が生じます。

    • Java環境外とのやりとり: JavaアプリケーションがJavaの外の環境とやりとりする必要がある場合があります。
    • オペレーティング・システムとのやりとり: JVMはJava言語そのものとランタイム・ライブラリをサポートしていますが、それでもなお、基盤となるシステム・サポートに依存する必要があることがあります。ネイティブ・メソッドは、Javaを使用して、Cで書かれたJVMの部分であるjreを実装する基盤となるシステムと対話することを可能にします。
    • SunのJava:SunのインタープリターはCの実装であり、普通のCのように外の世界と対話することができます。jreはほとんどJavaで実装されており、いくつかのネイティブ・メソッドを通して外の世界とも対話します。例えば、クラスjava.lang.ThreadのメソッドsetPriority()はJavaで実装されていますが、Cで実装されJVMに組み込まれているクラスのネイティブメソッドsetPriority()の呼び出しを実装しています。

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

    • Javaメソッド・コールを管理するためにJava VMスタックが使用されるのに対して、ローカル・メソッド・コールを管理するためにローカル・メソッド・スタックが使用されます。

    • ローカル・メソッド・スタックもスレッド・プライベートです。

    • スレッドに固定または動的に拡張可能なメモリー・サイズを許可します。

      • スレッドがローカル・メソッド・スタックの最大許容サイズを超えるスタック割り当てを要求すると、Java 仮想マシンは StackOverflowError 例外をスローします。
      • ローカル・メソッド・スタックが動的に拡張可能で、それを拡張しようとするときに十分なメモリーを要求しないか、新しいスレッドを作成するときに対応するローカル・メソッド・スタックを作成するのに十分なメモリーがない場合、Java 仮想マシンは OutofMemoryError 例外をスローします。
    • ネイティブ・メソッドはC言語で実装されています。

    • ネイティブ・メソッドはネイティブ・メソッド・スタックに登録され、ネイティブ・メソッド・ライブラリは実行エンジンの実行時にロードされます。 スレッドがネイティブ・メソッドを呼び出すと、VMの制限を受けない新しい世界に入ります。スレッドはVMと同じ権限を持ちます。

    • ローカル・メソッドは、ローカル・メソッド・インターフェースを介してVM内部のランタイム・データ領域にアクセスすることができ、ローカル・プロセッサのレジスタを直接使用したり、ローカル・メモリ・ヒープから直接任意の量のメモリを割り当てたりすることもできます!

    • すべてのJVMがローカル・メソッドをサポートしているわけではありません。これは、Java仮想マシンの仕様が、ネイティブ・メソッド・スタックのための言語、実装、データ構造などを明確に要求していないためです。JVM製品がネイティブ・メソッドをサポートするつもりがない場合、ネイティブ・メソッド・スタックを実装する必要はありません。

    スタックはランタイム・ユニットであり、ヒープはストレージ・ユニットです。

    スタックはプログラムの実行、つまり実行方法やデータの扱い方の問題を解決します。ヒープはデータの格納、つまりデータをどこにどのように置くかという問題を解決します。

    第四に、ヒープ・メモリ

    メモリ分割

    ほとんどのアプリケーションにとって、JavaヒープはJava仮想マシンによって管理され、すべてのスレッドによって共有されるメモリの最大の部分です。このメモリ領域の唯一の目的はオブジェクト・インスタンスを保持することであり、ほとんどすべてのオブジェクト・インスタンスとデータがここにメモリを割り当てられます。

    効率的なゴミ収集のために、仮想マシンはヒープ・メモリを論理的に3つの領域()に分割します:

    • 新しいゾーン:新しいオブジェクトや、ある年齢に達していないオブジェクトは新しい世代に属します。
    • 古い世代:長い間使用されてきたオブジェクトで、古い世代の方が若い世代よりもメモリ領域が大きいはずです。
    • メタ空間:いくつかのメソッドなどの一時的なオブジェクトは、JDK1.8は、JVMのメモリによって占有され、JDK1.8後に直接物理メモリを使用します。

    Java仮想マシンの仕様は、Javaのヒープは、ディスク領域のように、論理的に連続している限り、物理的に非連続のメモリ空間にすることができると述べています。インスタンスの割り当てがヒープ内で完了せず、ヒープをスケーリングできなくなった場合、OutOfMemoryError例外がスローされます。

    若い世代

    若い世代は、すべての新しいオブジェクトが作成される場所です。若い世代にオブジェクトが生成されると、ゴミ収集が行われます。このゴミ収集は マイナーGCと呼ばれます。ヤングジェネレーションは3つの部分-エデンの園と2つのサバイバーゾーン-に分けられ、デフォルトの比率は8:1:1です。

    • 新しく作成されたオブジェクトのほとんどはエデンのメモリ空間に配置されます。
    • エデン空間がオブジェクトで満たされると、MAGC実行され、すべてのサバイバーオブジェクトがサバイバー空間に移動します。
    • マイナーGCは生存オブジェクトをチェックし、別の生存空間に移動させます。そのため、毎回1つの生存者空間は常に空になります。
    • 複数のGCサイクルの後に生き残ったオブジェクトは、古い世代に移動されます。通常、これは、若い世代のオブジェクトが古い世代に昇格する資格を得る前に、年齢しきい値を設定することによって行われます。

    昔の話

    古い世代のメモリには、小さなGCを何度も繰り返して生き残ったオブジェクトが含まれます。通常、ゴミ収集は古い世代のメモリが一杯になったときに行われます。古い世代のゴミ回収はメインGCと呼ばれ、通常は時間がかかります。

    大きなオブジェクトは直接旧世代に入ります。この目的は、Eden領域と2つのSurvivor領域の間で多くのメモリコピーを避けるためです。

    メタスペース

    JDK8のパーマネント世代とJDK8以降のメタスペースは、どちらもJava仮想マシン仕様のメソッドゾーンの実装と見なすことができます。

    Java VM仕様では、メソッド領域はヒープの論理的な一部として記述されていますが、Javaヒープと区別するためにNon-Heapという別名があります。

    そのため、メタスペースはメソッド領域の後に配置されます。

    ヒープ・メモリ・サイズとOOMの設定

    JavaヒープはJavaオブジェクト・インスタンスを格納するために使用されるため、ヒープのサイズはJVMの起動時に決定され、-Xmxと-Xmsで設定できます。

    • -Xmxは、ヒープの初期サイズを示すために使用され、-XX:InitialHeapSizeに相当します。
    • -Xmsは、ヒープの最大サイズを示すために使用され、-XX:MaxHeapSizeに相当します。

    ヒープのサイズが-Xmsで設定された最大メモリを超えた場合、OutOfMemoryError例外がスローされます。

    Xmxと-Xmsを同じ値に設定するのは、ゴミ収集メカニズムがヒープをクリーンアップした後にヒープサイズを再分離する必要をなくし、パフォーマンスを向上させるためです。

    • デフォルトでは、初期ヒープ・サイズはコンピュータ・メモリ/64 です。

    • デフォルトでは、最大ヒープ・サイズはコンピュータ・メモリ / 4 です。

    設定はコードで取得でき、もちろんOOMのシミュレーションも可能です:

    public static void main(String[] args) {
     //JVMヒープ・サイズを返す
     long initalMemory = Runtime.getRuntime().totalMemory() / ;
     //JVMヒープの最大メモリを返す
     long maxMemory = Runtime.getRuntime().maxMemory() / ;
     System.out.println("-Xms : "+initalMemory + "M");
     System.out.println("-Xmx : "+maxMemory + "M");
     System.out.println("システム・メモリ・サイズ:" + initalMemory *  + "G");
     System.out.println("システム・メモリ・サイズ:" + maxMemory * 4 / 1024 + "G");
    }
    

    JVMヒープ・メモリ割り当ての表示

    1. JVMヒープ・メモリ・サイズがデフォルトで設定されていない場合、JVMはデフォルト値に基づいて現在のメモリ・サイズを割り当てます。

    2. デフォルトでは、新しい世代と古い世代の比率は1:2で、-XX:NewRatioで設定できます。

    3. maximum local variablesJDK 7で有効にすると、JVMはJVMヒープの各領域のサイズと古い世代に入る年齢を動的に調整します。

      この場合、-XX:NewRatioと-XX:SurvivorRatioは無効になりますが、JDK 8ではデフォルトでオンになっています。maximum local variables

      maximum local variablesJDK 8では、ヒープメモリの分割方法について明確な計画がない限り、オフにしないでください

    Eden、From Survivor、To SurvivorのサイズはGCのたびに再計算されます。

    計算は、GC時間スループットGCプロセス中のメモリ使用量に基づいて行われます。

    java -XX:+PrintFlagsFinal -version | grep HeapSize
     uintx ErgoHeapSizeLimit = 0 {product}
     uintx HeapSizePerGCThread =  {product}
     uintx InitialHeapSize :=  {product}
     uintx LargePageHeapSizeThreshold =  {product}
     uintx MaxHeapSize :=  {product}
    java version "1.8.0_211"
    Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
    Java HotSpot(TM) 64-Bit Server VM (build -b12, mixed mode)
    
    jmap -heap プロセス番号

    ヒープ内のオブジェクト・ライフサイクル

    1. ヒープのJVMメモリ・モデルでは、ヒープは新しい世代と古い世代に分けられます。
    2. オブジェクトが作成されると、新世代のエデン・セクションに優先的に割り当てられます。
      • maximum local variablesこの時点で、JVMはオブジェクトのためのオブジェクトユースカウンター( )を定義島津
    3. エデンに十分なスペースがない場合、JVMは新しい世代でゴミ収集を実行します。
      • JVMは生き残ったオブジェクトをSurvivorに移し、オブジェクト年齢は+1されます。
      • SurvivorのオブジェクトもMinor GCを経て、Minor GCごとにオブジェクトの年齢は+1されます。
    4. maximum local variablesもし、割り当てられたオブジェクトの年齢1年齢超えると、そのオブジェクトは直接古い年齢に割り当てられます

    オブジェクトの割り当てプロセス

    オブジェクトのメモリを割り当てることは、非常に厳密で複雑なタスクです。 JVM設計者は、メモリを割り当てる方法と場所を考慮する必要があるだけでなく、メモリ割り当てアルゴリズムがメモリ再生アルゴリズムと密接に関連しているため、GCが実行された後にメモリ空間にメモリの断片化が発生するかどうかも考慮する必要があります。

    1. 新しいオブジェクトは、まずサイズ制限のあるEden領域に配置されます。
    2. エデンの領域が一杯になり、プログラムがオブジェクトを作成する必要がある場合、JVMのゴミ収集器がエデンのオブジェクトをゴミ収集し、他のオブジェクトから参照されなくなったエデンのオブジェクトを破棄します。それから新しいオブジェクトをエデンにロードします。
    3. その後、Edenゾーンに残っているオブジェクトをSurvivor 0ゾーンに移動します。
    4. 再びゴミ収集が発生した場合、生き残ったオブジェクトは生存者0に、収集されなかったオブジェクトは生存者1に移動します。
    5. 再びゴミ回収が発生した場合、生き残ったオブジェクトはサバイバー0に戻され、その後サバイバー1に戻されます。
    6. いつ引退ゾーンに行きますか? デフォルトは15リサイクル・マーカーです。
    7. リタイアゾーンでは比較的簡単です。リタイアメントゾーンのメモリが足りなくなると、リタイアメントゾーンのメモリを一掃するためにメジャーGCが再度発動されます。
    8. 引退領域でメジャーGCが実行され、それでもオブジェクトが保存できない場合、OOM例外がスローされます。

    GC ガベージコレクション入門

    マイナーGC、メジャーGC、フルGC

    JVMがGCを実行するとき、それは常に一緒にヒープメモリ領域をリサイクルしません、ほとんどの場合、回収は新しい世代を参照します。

    HotSpot VMの実装では、GCは再利用される領域によって2つのカテゴリに分けられます。

    • 部分回収:Javaヒープ全体を回収しないガベージコレクション。これはさらに次のように分けられます:
      • 新世代コレクション:新世代のゴミ回収のみ。
      • 旧世代収集:旧世代のゴミ収集のみ。
        • 現在、最も古い世代を個別に収集する動作を持っているのはCMS GCだけです。
        • 多くの場合、Major GCとFull GCが混在しているので、旧世代回収かフルヒープ回収かを区別する必要があります。
      • 混合収集:すべての新しい世代といくつかの古い世代のごみ収集。
        • 現在、G1 GCだけがこの動作を持っています。
    • フルヒープ収集:Javaヒープ全体とメソッド領域からゴミを収集します。

    TLAB

    TLAB

    • ゴミ収集の代わりに、JVMは各スレッドにプライベート・キャッシュ領域を割り当てることで、エデン領域の分割を継続し、エデン領域内に収めます。
    • 複数のスレッドに同時にメモリを割り当てるとき、TLABを使用すると、スレッドセーフでない多くの問題が回避され、メモリ割り当てのスループットが向上するので、これを高速割り当て戦略と呼ぶことができます。
    • OpenJDKから派生したJVMのほとんどは、TLAB設計を提供しています。

    TLAB ?

    • ヒープはスレッド間で共有され、どのスレッドもヒープ内の共有データにアクセスできます。
    • オブジェクト・インスタンスはJVM内で非常に頻繁に生成されるため、同時実行環境でヒープからメモリ空間を分割することはスレッドセーフではありません。
    • 複数のスレッドが同じアドレスで動作するのを避けるために、ロックのようなメカニズムを使用する必要があり、これは割り当ての速度に影響します。

    すべてのオブジェクト・インスタンスがTLABでうまく割り当てられるわけではありませんが、JVMはメモリ割り当ての最初の選択肢としてTLABを使用します。

    アプリケーションでTLAB空間を有効にするかどうかは、-XX:UseTLABで設定できます。

    maximum local variables デフォルトでは、TLABスペースは非常に小さく、Edenスペース全体の1%しか占めません。

    オブジェクトがTLABスペースでメモリを割り当てられない場合、JVMはEdenスペースで直接メモリを割り当てるためにロック機構を使ってアトミック性を確保しようとします。

    オブジェクト・ストレージの割り当てにはヒープしかないのですか?

    JITコンパイル期間が進化し、エスケープ分析技術が成熟するにつれて、スタック上の割り当てやスカラー置換などの最適化技術は微妙な変化をもたらし、すべてのオブジェクトをヒープに割り当てることは徐々に「絶対的」ではなくなるでしょう。 --Java仮想マシンの理解

    エスケープ解析

    エスケープ解析は、Java仮想マシンで現在利用可能な、より最先端の最適化技術の1つです。これはクロスファンクションのグローバルデータフロー解析アルゴリズムで、Javaプログラムの同期負荷とメモリヒープ割り当て圧力を効果的に軽減します。エスケープ分析では、Java Hotspotコンパイラは新しいオブジェクトへの参照のスコープを分析し、オブジェクトをヒープに割り当てるかどうかを決定することができます。

    エスケープ分析の基本的な動作は、オブジェクトの動的スコープを分析することです:

    • オブジェクトがメソッド内で定義され、そのオブジェクトがメソッド内でのみ使用される場合、エスケープは発生しないとみなされます。
    • オブジェクトがメソッド内で定義され、外部のメソッドから参照される場合、エスケープが発生したとみなされます。例えば、呼び出しパラメータとして他の場所に渡すことは、メソッドエスケープと呼ばれます。

    $ jmap -heap  
    

    StringBuffer sb はあるメソッドの内部変数ですが、上記のコードでは sb を直接返しているため、この StringBuffer は他のメソッドで変更される可能性があります。他のスレッドでアクセスできるクラス変数やインスタンス変数に代入するなどして、外部のスレッドからアクセスされる可能性もあります(これをスレッドエスケープと呼びます)。

    上記のコードは、StringBuffer sb をメソッドからエスケープさせたくない場合は、次のように記述します:

    public static StringBuffer craeteStringBuffer(String s1, String s2) {
     StringBuffer sb = new StringBuffer();
     sb.append(s1);
     sb.append(s2);
     return sb;
    }
    

    StringBufferを直接返す代わりに、StringBufferをメソッドからエスケープしないようにします。

    パラメータの設定

    • JDK6u23以降、HotSpotではエスケープ解析がデフォルトで有効になっています。
    • maximum local variablesそれ以前のバージョンを使用している場合は、.

    開発で使用する場合は、メソッドの外でローカル変数を定義しないでください。

    エスケープ解析を使うと、コンパイラがコードを最適化できます:

    • スタック上の割り当て:ヒープ割り当てをスタック割り当てに変換します。オブジェクトがサブルーチン内で割り当てられ、そのオブジェクトへのポインタが決してエスケープされない場合、そのオブジェクトはヒープ割り当てではなくスタック割り当ての候補になる可能性があります。
    • あらすじ:オブジェクトが1つのスレッドからのみアクセス可能であることが判明した場合、そのオブジェクトに対する操作は同期に関係なく実行できます。
    • 分離されたオブジェクトやスカラー置換:オブジェクトの中には、連続したメモリ構造として存在しなくてもアクセスできるものがあります。

    JITコンパイラは、オブジェクトがメソッドからエスケープされないことを発見した場合、エスケープ解析の結果に基づいて、コンパイル時にオンスタックアロケーションを最適化することができます。割り当てが完了すると、呼び出しスタック上で実行が続行され、最終的にスレッドが終了し、スタック領域が回収され、ローカル変数オブジェクトが回収されます。これにより、ゴミ収集の必要がなくなります。

    オンスタック・アロケーションの一般的なシナリオ:メンバ変数の割り当て、メソッドの戻り値、インスタンス参照渡し

    コードの最適化:同期の省略
    • スレッドの同期にはコストがかかり、その結果、同時実行性とパフォーマンスが低下します。
    • 同期ブロックを動的にコンパイルするとき、JITコンパイラはエスケープ分析を使って、同期ブロックが使用するロック・オブジェクトが他のスレッドに解放されることなく1つのスレッドがアクセスできるかどうかを判断できます。そうでない場合、JITコンパイラはブロックをコンパイルする際にコードを非同期化します。これにより、同時実行性とパフォーマンスが大幅に向上します。この非同期化のプロセスは省略と呼ばれ、ロック除去としても知られています。
    public static String createStringBuffer(String s1, String s2) {
     StringBuffer sb = new StringBuffer();
     sb.append(s1);
     sb.append(s2);
     return sb.toString();
    }
    

    上記のコードのように、コードは Keeper オブジェクトをロックしていますが、Keeper オブジェクトのライフサイクルは keep() メソッドの中だけであり、他のスレッドからアクセスされることはありません。に最適化されます:

    public void keep() {
     Object keeper = new Object();
     synchronized(keeper) {
     System.out.println(keeper);
     }
    }
    
    コード最適化のためのスカラー置換

    スカラーとは、もはや小さなデータ片に分解できないデータ片のことです。Javaの元のデータ型はスカラーでした。

    これに対して、より小さなデータの断片に分解できるデータは集約と呼ばれ、Javaのオブジェクトは他の集約やスカラーに分解できるので集約です。

    JITフェーズでは、エスケープ分析によってオブジェクトが外部からアクセスされることはなく、オブジェクトをさらに分解できると判断された場合、JVMはオブジェクトを作成せず、代わりにオブジェクトのメンバ変数を、このメソッドで使用されるメンバ変数の多数の置換に分解します。これらの置換は、スタック・フレームまたはレジスタにスペースが割り当てられます。この処理がスカラー置換です。

    maximum local variablesmaximum local variables でスカラー置換をオンにし、 .NET でスカラー置換を表示できます。

    public void keep() {
     Object keeper = new Object();
     System.out.println(keeper);
    }
    

    上記のコードでは、Pointオブジェクトはalloc()メソッドをエスケープしておらず、スカラーに分割することができます。そのため、JIT は Point オブジェクトを直接生成せず、2 つのスカラー int x と int y を使って Point オブジェクトを置き換えます。

    public static void main(String[] args) {
     alloc();
    }
    private static void alloc() {
     Point point = new Point;
     System.out.println("point.x="+point.x+"; point.y="+point.y);
    }
    class Point{
     private int x;
     private int y;
    }
    
    スタックへのアロケーションのコード最適化

    JVMのメモリ割り当てを通じて、私たちはJAVAのオブジェクトがヒープ上に割り当てられることを知ることができ、オブジェクトが参照されないとき、私たちはメモリを取り戻すためにGCに頼る必要があり、オブジェクトの数が多ければ、それはGCに多くの圧力をもたらし、間接的にアプリケーションのパフォーマンスに影響を与えます。ヒープに割り当てられた一時オブジェクトの数を減らすために、JVMはエスケープ分析を通じて、オブジェクトが外部からアクセスされないことを決定します。その後、オブジェクトは、スタック上のメモリを確保するためにスカラー置換によって分解されるので、オブジェクトによって占有されるメモリ空間は、スタックフレームのスタックアウトで破壊することができ、それはゴミ収集の圧力を低減します。

    まとめ:

    エスケープ分析に関する論文は1999年に発表されましたが、実装されたのはJDK 1.6からで、現在に至るまでこの技術はあまり成熟していません。

    その根本的な理由は、エスケープ解析の性能消費が消費量を上回る保証がないからです。エスケープ解析はスカラー置換、スタック割り当て、ロック除去を行うことができますが。しかし、エスケープ解析自体も一連の複雑な解析を行う必要があり、実際には比較的時間のかかる処理です。

    極端な例としては、エスケープ解析の結果、エスケープしないオブジェクトが存在しないことが判明した場合です。これでは、せっかくの脱出分析が無駄になってしまいます。

    この技法はあまり成熟していませんが、オンザフライ・コンパイラ最適化技法の非常に重要な手段でもあります。

    V. メソッド領域

    • メソッド領域は、Javaヒープと同様に、すべてのスレッドで共有されるメモリ領域です。
    • Java仮想マシンの仕様では、メソッド領域はヒープの論理的な一部として記述されていますが、Javaヒープと区別するためにNon-Heapという別名があります。
    • ランタイム定数プールはメソッド領域の一部です。Classファイルには、クラス・バージョン/フィールド/メソッド/インタフェースやその他の記述情報だけでなく、コンパイル時に生成されたさまざまなリテラルやシンボリック参照を保持する定数プールが含まれており、クラスがロードされた後にメソッド領域に入るランタイム定数プールに格納されます。この機能は、String.intern() メソッドよりも開発者に利用されています。メソッド領域のメモリ制限のため、定数プールがメモリを要求できなくなると OutOfMemoryError 例外がスローされます。
    • メソッド領域のサイズはヒープ領域と同じで、固定サイズまたは拡張可能なサイズを選択できます。メソッド領域のサイズは、システムに置くことができるクラスの数を決定します。
    • メソッド領域は、JVMが終了した後に解放されます。

    問題の解決

    • メソッドドメイン、クラス情報、定数プール、静的変数、JITコンパイル済みコードなどのデータを格納するために JVM 定義概念にスギマセン永続生成は Hotspot 仮想マシン独自の概念で、Java8ではメタ空間に置き換えられ、永続生成とメタ空間は着地実装のメソッド領域と理解できます。
    • 永久世代は物理的にヒープの一部であり、新旧世代と連続していますが、メタ空間はローカルメモリに存在し、JVMによって制限されず、OOMを引き起こしにくくなっています。
    • maximum local variables Java7では、-XX:PermSizeと-xx:MaxPermSizeがperma-sizeパラメータを設定するために使用されていましたが、Java8以降、perma-sizeのキャンセルに伴い、これらのパラメータは無効になり、-XX:MetaspaceSizeとmetaspaceパラメータを設定するために置き換えられました。
    • メタスペースにはクラスのメタ情報が格納され、静的変数や定数プールはヒープにマージされます。パーマネント世代に相当するデータは、ヒープとメタスペースに分割されます。
    • メソッド領域のメモリーが割り当て要求を満たすために使用できない場合、Java VMはOutOfMemoryErrorをスローします。
    • JVMの仕様では、メソッド領域は論理的にはヒープの一部ですが、現在はJavaヒープとは事実上分離されています。

    そのため、メソッド領域については、Java 8以降に変更があります:

    • 永続生成が削除され、メタ空間に置き換えられました;
    • 永続世代のクラス・メタデータはネイティブ・メモリに移動しました;
    • 永続世代のインターンされた文字列とクラスの静的変数は、Javaヒープに移動しました;
    • 永続化のパラメータ → メタ空間のパラメータ

    メソッド・メモリのサイズの設定

    JDK8以降:

    • maximum local variables メタデータ領域のサイズは、上記の2つのパラメータの代わりに -XX:MetaspaceSize パラメータを使用して指定できます。
    • maximum local variables デフォルト値はプラットフォームに依存します。Windowsでは、-XX:MetaspaceSizeは21Mで、-1、つまり制限はありません!
    • 永続世代とは異なり、サイズが指定されない場合、デフォルトで仮想マシンは使用可能なシステム・メモリをすべて使用します。メタデータのオーバーフローが発生すると、VMは例外をスローします。 maximum local variables
    • -XX:MetaspaceSize : メタ空間の初期サイズを設定します。64ビットのサーバーサイドJVMの場合、-XX:MetaspaceSizeのデフォルト値は20.75MBで、これが最初のハイウォーターマークです。 このハイウォーターマークに達すると、Full GCがトリガーされて無駄なクラスをアンロードし、ハイウォーターマークがリセットされ、新しいハイウォーターマークの値は、GC後に解放されるメタスペースの量に依存します。十分なスペースが解放されない場合は、MaxMetaspaceSizeを超えない範囲で値を適切に増やします。解放される領域が多すぎる場合は、値を適切に下げます。
    • 初期化されたハイ・ウォーター・マークが低く設定されすぎると、上記のハイ・ウォーター・マークの調整が何度も起こり、ゴミ収集ログからFull GCの複数回の呼び出しが観測される可能性があります。頻繁なGCを避けるには、-XX:MetaspaceSizeを比較的大きな値に設定することをお勧めします。

    メソッドエリア内部構造

    タイプ・インテリジェンス

    ロードされた型ごとに、JVMは以下の型情報をメソッド領域に格納する必要があります。

    • 型の完全な有効名
    • 型の直接の親の完全な有効名
    • 型の修飾子
    • 型の直接インターフェースの順序付きリスト

    ドメイン情報

    • JVMは、型のすべてのドメインと、それらがメソッド領域で宣言された順序に関する情報を格納する必要があります。
    • ドメイン情報には、ドメイン名、ドメイン型、ドメイン修飾子が含まれます。

    メソッド情報

    JVMは、すべてのメソッド

    • メソッド名
    • メソッドの戻り値の型
    • メソッドのパラメーターの数とタイプ
    • メソッドの修飾子
    • 文字コード、オペランド・スタック、ローカル変数テーブル、メソッドのサイズ
    • 例外テーブル
      • 各例外ハンドラの開始位置と終了位置、プログラム・カウンタ内のコード・ハンドラのオフセット・アドレス、キャッチされた例外クラスの定数プールのインデックス

    スタック、ヒープ、メソッド領域の相互作用

    ランタイム定数プール

    ランタイム定数プールはメソッド領域の一部です。 ランタイム定数プールを理解するために、まずバイトコード・ファイルの定数プールについて説明しましょう。

    固定数プラン

    有効なバイトコードファイルには、クラスのバージョン情報、フィールド、メソッド、インタフェースの記述に加えて、様々なリテラルと型、フィールド、メソッドへのシンボリック参照を含む定数プールのテーブルが含まれています。

    なぜ定数プールが必要なのですか?

    Java ソース・ファイル内のクラスとインタフェースは、バイトコード・ファイルを生成するためにコンパイルされます。Javaのバイトコードはデータによってサポートされる必要があり、通常、バイトコードに直接格納できないほど大きいのですが、別の方法で、定数プールへの参照を含む定数プールに格納することができます。このバイトコードには定数プールへの参照が含まれています。 これは動的リンクで使用される実行時定数プールです。

    例えば、Main メソッドだけの単純なクラスを jclasslib 経由で見ると、バイトコードの #2 は定数プールを指しています。

    定数プールは、VM 命令が実行するクラス名、メソッド名、パラメータ型、リテラルなどを見つけるテーブルと考えることができます。

    ランタイム定数プール

    • クラスと構造体を仮想マシンにロードした後、対応するランタイム定数プールが作成されます。
    • 定数プール・テーブルはクラス・ファイルの一部で、コンパイル時に生成された様々なリテラルやシンボリック参照を格納するために使用されます。これらは、クラスがロードされた後、メソッド領域のランタイム定数プールに格納されます
    • JVMは、ロードされた型ごとに定数プールを維持します。プール内のデータ項目は、配列項目のようにインデックスによってアクセスされます。
    • ランタイム定数プールには、コンパイラによって明示される数値リテラルや、ランタイム・パーシングの後まで利用できないメソッドやフィールド参照など、さまざまな定数が含まれています。これはもはやプール内のシンボリック・アドレスではなく、実際のアドレスです。
      • クラス・ファイルの定数プールと比較して、実行時定数プールのもう1つの重要な特徴は、ダイナミックあるということです。 Java言語では、定数はコンパイル時にのみ生成されなければならないとは要求されておらず、Stringクラスのintern()メソッドのように、実行時に新しい定数をプールに入れることができます。
    • クラスまたはインタフェースの実行時定数のプールを作成するとき、プールを構築するために必要なメモリ領域がメソッド領域で利用可能な最大値を超えると、JVMはOutOfMemoryError例外をスローします。

    JDK 6、7、8 におけるメソッド領域の進化 詳細

    HotSpotのみが永続的生成の概念を持っています。

    jdk1.7 には永続性がありますが、文字列定数プールと静的変数が削除され、ヒープに格納されるなど、徐々に「非永続化」されています。
    jdk1.8 永続生成は解除され、型情報、フィールド、メソッド、定数はローカルメモリのメタ空間に格納されますが、文字列定数プール、静的変数は依然としてヒープに格納されます。

    永続生成が解除された理由

    • パーマリンクのスペースサイズの設定は難しいです。

      シナリオによっては、動的にロードされるクラスが多すぎる場合、Perm領域でOOMが発生しやすくなります。実際のWebプロジェクトが多くのファンクションポイントを持つ場合、実行時に多くのクラスを動的にロードし続けなければならず、OOMがしばしば発生します。のサイズによってのみ制限されます。

    • 永続生成のチューニングはより難しい

    メソッド領域のガベージコレクション

    メソッド領域のガベージコレクションは、定数プールで非推奨になった定数と、使われなくなった型の2つの主な要素を収集します。

    まず、メソッド領域の定数プールに格納されている2つの主な定数、リテラルとシンボリック参照について説明します。リテラルは、テキスト文字列やfinalとして宣言された定数など、Javaレベルでの定数の概念に近いものです。一方、シンボリック参照はコンパイルの原則の概念に属し、以下の3種類の定数が含まれます:

    • クラスとインターフェースの完全修飾名
    • フィールド名と記述子
    • メソッド名と記述子

    定数プールに対する HotSpot VM のリサイクルポリシーは明確で、プール内の定数がどこにも参照されていない限り、リサイクルすることができます。

    ある型が「使われなくなったクラス」かどうかを判断するには、3つの条件を満たす必要があります:

    • そのクラスのすべてのインスタンスが再生利用されている、つまり、Java ヒープにそのクラスまたはその派生サブクラスのインスタンスがないこと。
    • これは、OSGi や JSP のリロードなど、クラス・ローダを置き換えることができる注意深く設計されたシナリオを除いて、しばしば達成するのが困難です。
    • クラスに対応するjava.lang.Classオブジェクトはどこにも参照されず、クラスのメソッドはリフレクションによってどこにもアクセスできません。

    Java仮想マシンは、上記の3つの条件を満たす無用なクラスをリサイクルすることが許可されていますが、これはあくまで「許可されている」という話であり、使用されなくなったときにリサイクルされるオブジェクトとは異なります。 maximum local variablesmaximum local variables HotSpot VMには、クラスがリサイクルされるかどうかを制御する-Xnoclassgcパラメータが用意されており、-verbose:classとandを使用してクラスのロードとアンロードの情報を表示することもできます。

    リフレクション、動的プロキシ、CGLib などの ByteCode フレームワーク、動的に生成される JSP、OSGi などの頻繁にカスタマイズされる ClassLoaders を多用するシナリオでは、永続的な生成がオーバーフローしないように、VM にクラスのアンロード機能が必要です。

    Read next

    バブルソートと選択ソート

    要素の配列は、比較する左端とその右の要素を取り、左が大きい場合は、位置を交換し、右が大きい場合は、交換しないでください;そして、比較する2番目の要素と3番目の要素を取り、2番目の大きい場合は、位置を交換し、3番目の大きい場合は、交換しないでください;最後の1つまで。 6データ、外側のループは5回、内側のループは5,4,3,2,1回です。 arrの長さは6、iから...

    Dec 24, 2020 · 5 min read