blog

JVM-ランタイム・データ領域-ヒープ

プロセスは、jvmインスタンス、実行時データ領域に対応し、順番にメソッド領域とヒープを共有する複数のスレッドを含み、各スレッドにはプログラムカウンタ、ローカルメソッドスタック、仮想マシンスタックが含ま...

Jun 28, 2020 · 20 min. read
シェア

プロセスは jvm インスタンス、実行時データ領域に対応し、そのデータ領域はメソッド領域とヒープを共有する複数のスレッドを含み、各スレッドはプログラムカウンタ、ローカルメソッドスタック、仮想マシンスタックを含みます。

カーネルの概要

  • jvmインスタンスのヒープ・メモリは1つだけであり、ヒープはJavaのメモリ管理のコア領域でもあります。
  • Javaヒープは、JVMの起動時に作成され、そのサイズが決定されます。これは、JVMによって管理されるメモリー空間の最大の部分です。
  • Java仮想マシン仕様では、ヒープは物理的には非連続なメモリ空間に存在することができますが、論理的には連続なメモリ空間として扱われるべきであるとされています。
  • すべてのスレッドはjavaヒープを共有し、スレッドプライベートバッファも分割することができます。
  • Java仮想マシン仕様では、Javaヒープを、すべてのオブジェクト・インスタンスと配列が実行時に割り当てられるべき場所と説明しています。実用的な観点からは、オブジェクトの「ほとんど」すべてのインスタンスがここに割り当てられています!
  • 配列やオブジェクトがスタックに格納されることはありません。スタック・フレームは、ヒープ内のオブジェクトや配列の位置を指す参照を保持するからです。
  • ヒープ内のオブジェクトは、メソッドが終了した直後には削除されません。
  • GC(ガベージ・コレクション)がゴミを収集するための領域であるヒープ。

ヒープの細分化されたメモリー構造

JDK 7

新生ゾーン+引退ゾーン+パーマネントゾーンに論理的に分割

  • ヤングジェネレーション・スペース:エデンゾーンとサービオゾーンに細分化。
  • Tenure generation Space ==Old/Tenure==
  • Permanent Space ==Perm==
JDK 8

JDK 8以降: 新入生エリア + 旧入生エリア + メタスペースに論理的に分割 (つまり、Xms/Xmxに割り当てられたメモリは物理的にメタスペースに関与しません)

  • ヤングジェネレーション・スペース:エデンゾーンとサービオゾーンに細分化。
  • Tenure generation Space ==Old/Tenure==
  • Meta Space ==Meta==

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

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

    • -Xmsは、ヒープ空間の初期メモリサイズを設定するために使用され、-XX:InitialHeapSizeに相当します。
      • -X はjvmの実行時パラメータです。
      • ms はメモリスタート
    • -Xmxは、ヒープの最大メモリを設定するために使用され、-XX:MaxHeapSizeに相当します。
  • ヒープ内のメモリ・サイズが-Xmxで指定された最大メモリを超えると、OOM例外がスローされます。

    • デフォルトでは、初期メモリサイズ:物理メモリサイズ/64、最大メモリサイズ:物理メモリサイズ/4。
    • 手動設定: -Xms600m -Xmx600m
  • Xmsと-Xmxパラメータは通常同じ値で設定されます。その目的は、javaのゴミ収集メカニズムによってクリーンアップされた後、ヒープのサイズを再分割して計算する必要がないため、パフォーマンスを向上させることができます。

    • 例えば、デフォルトの空きヒープ・メモリが40%未満の場合、JVMは-Xmxの最大制限までヒープを増やします。したがって、サーバーは通常、-Xmsと-Xmxを等しく設定して、各GC後にヒープのサイズを変更しないようにします。
  • 設定されたヒープメモリパラメータを表示します:

    • 方法1:ターミナルでjpsと入力し、jstat -gcプロセスIDを入力します。
    • 方法2:コンフィギュレーションを編集する>VM Options XXを追加:+PrintGCDetails

ヒープサイズ分析

ヒープサイズを600mに設定すると、575mと出力されます。これは、サバイバーゾーンS0とS1がそれぞれ25mを占めますが、片方は常に空で、エデンゾーンとサバイバーゾーンにオブジェクトが格納されているためです。

若い世代と古い世代

  • JVMに格納されているJavaオブジェクトは、2つのカテゴリーに分けられます:

    • 一つのクラスは、ライフサイクルの短い一時的なオブジェクトです。
    • 他のクラスのオブジェクトはライフサイクルが非常に長く、場合によってはJVMのライフサイクルに合わせることができます。
  • Javaヒープ領域は、さらに若い世代と古い世代に細分化できます。若い世代は、Eden空間、Survivor0空間、Survivor1空間に分けられます。

  • ヒープ構造における新旧世代の割合の構成

    • デフォルト -XX: NewRatio=2、これは新世代が1、旧世代が2、新世代がヒープ全体の1/3を占めることを意味します。
    • XX:NewRatio=4を変更することで、新世代が1、旧世代が4、新世代がヒープ全体の1/5を占めることを示すことができます。
  • hotSpotでは、Edenスペースと他の2つのSurvivorスペースのデフォルトの比率は8:1:1です。開発者は、-XX:SurvivorRatio=8のように、-XX:SurvivorRatioオプションでスペース比率を調整できます。

  • ほとんどのJavaオブジェクトは、エデン・エリアで新しく作成されます。

  • Javaオブジェクトの大半は新世代で破壊されます。

  • オプション**-Xmn**を使用すると、新世代の最大メモリサイズを設定できます。

IV.オブジェクト割り当ての一般的なプロセス

一般的なプロセス

1.新しいオブジェクトは、まずエデン領域を配置します。このエリアにはサイズ制限があります。

2.エデンの園のスペースがいっぱいになると、プログラムはオブジェクトを作成する必要があり、JVMのごみコレクタは、エデンの園のごみ収集**(マイナーGC)の領域になり、エデンの園の領域では、もはやオブジェクトの破壊のために他のオブジェクトによって参照されていません**。その後、エデンに新しいオブジェクトをロードします。

3.その後、エデンに残っている生存オブジェクトを生存者0エリアに移動します。

4.再びゴミ収集が発生した場合、サバイバー0エリアに置かれたサバイバーは、収集されなければサバイバー1エリアに置かれます。

5.再びゴミ収集が行われた場合、この時点でサバイバー0に戻され、その後サバイバー1へ。

6.いつ引退エリアに行けますか?回数を設定できます。デフォルトは15回です。-XX:MaxTenuringThreshold=で設定できます。

7.高齢者エリアでは、比較的のんびり。高齢者エリアがメモリ不足になると、高齢者エリアのメモリクリーンアップのためにGC:Major GCが再度起動されます。

8.古い年代のゾーンでメジャーGCが実行された後、まだオブジェクトを保存できないことが判明した場合、OOM例外がスローされます。

要約すると:生存者s0,s1ゾーンの場合:レプリケーションの後に交換があり、空になった人がTOになります。 ゴミ収集の場合:新入生ゾーンでは頻繁に収集、年金受給者ゾーンでは稀に収集、常設ゾーン/メタスペースでの収集はほぼなし。

注:エデンが満員の場合のみ、minorGC/youngGCが発動し、サバイバーゾーンが満員の場合はminorGCが発動することはありません。

オブジェクト割り当ての特殊なケース

YGCの後、大型の物体がまだ新しい世代に入れられない場合は、古い世代に直接入れられます。

一般的なチューニングツール

  • JDK
  • Eclipseメモリ解析ツール
  • Jconsole
  • VisualVM
  • Jprofiler
  • Java Flight Recorder
  • GCViewer
  • GC Easy

Minor GCメジャーGC、フルGC

GCのJVMは、上記の3つのメモリ領域を一緒に回復するためのすべての時間ではなく、時間の回復のほとんどは、新しい世代を指します。

hotSpot VMの実装では、GCはリカバリ領域によって大きく2つのタイプに分けられます。

  • 部分収集:Javaヒープ全体の完全な収集ではないゴミ収集。さらに次のように分けられます:
    • 新世代コレクション: 新世代だけのゴミコレクション
    • 旧世代コレクション: 旧世代のゴミ収集のみ
      • 現在のところ、CMS GCだけが古い世代を個別に収集する振る舞いをします。
      • Major GCはFull GCと混同されることが多いので、旧世代収集かフルヒープ収集かを区別する必要があることに注意してください。
    • 混合収集: 新世代全体と旧世代の一部を収集します。
      • 混合コレクションはメソッド・リカバリを伴わず、新世代と旧世代のガベージ・コレクションが混在しているだけです。
      • 現在のところ、G1 GCは以降この挙動になります。
  • ヒープ全体収集:Javaヒープとメソッド領域全体を収集するゴミ収集。

さまざまなGCの誘発メカニズム

若い世代のGCトリガーメカニズム
  • マイナーGCは若い世代がスペース不足になったときに発動され、若い世代が一杯になるとエデン世代が一杯になり、サバイバー世代が一杯になってもGCは発動されません。
  • Javaのキューのほとんどは、夜と昼の特性を持っているので、MonorのGCは非常に頻繁に、一般的に高速なリカバリは、この定義は明確で理解しやすいのです。
  • マイナーGCはSTWをトリガーし、他のユーザーのスレッドをサスペンドし、ユーザーのスレッドが実行を再開する前にゴミ収集が終了するのを待ちます。
老年期GC(メジャーGC/フルGC)トリガーメカニズム
  • メジャーGCまたはフルGCは、オブジェクトが老齢で消滅するときに発生します。
  • Major GCが発生すると、多くの場合、少なくとも1回のMinor GCを伴います。
    • つまり、老年期に十分な空き容量がない場合、まずMinor GCを発動しようとし、それでもまだ十分な空き容量がない場合、Major GCを発動することになります。
  • メジャーGCは一般的にマイナーGCの10倍以上遅く、STW時間も長くなります。
  • Major GC後もメモリ不足の場合、OOMが報告されます。
Full GCトリガーメカニズム
  • フルGCの実行のトリガーとなる状況は、以下の5つです。
    • System.gc()が呼び出されたとき、システムはFull GCの実行を提案しますが、必ずしも実行するわけではありません。
    • 古い世代におけるスペース不足
    • メソッドエリアのスペース不足
    • マイナーGC後の旧世代の平均サイズが旧世代の空きメモリより小さい
    • Eden 領域、Survivor S0 領域から S1 領域へのコピーの際、オブジェクトのサイズが To Space の空きメモリに起因し、その後オブジェクトが old age にダンプされ、old age の空きメモリがオブジェクトのサイズより小さい。
  • 注:フルGCは開発やチューニングでは避けるべきもので、間を短くします!

ヒープ空間生成のアイデア

なぜJavaのヒープをサブ生成する必要があるのですか?置換なしには動かないのですか?

  • モノによってライフサイクルが異なることは研究されています。
    • 新世代:エデン、生存者の構成があり、常に空です。
    • 旧世代:まだ何度も後に生き残る新世代のオブジェクトを格納します。
  • 実際には、サブ生成しないことは全く問題ありません、サブ生成する唯一の理由は、GCのパフォーマンスを最適化することです。世代がない場合は、ピースのすべてのオブジェクトは、人々の学校のように教室に閉じ込められています。GCの時間は、オブジェクトが使用されていない見つけるために、それはスキャンするすべての領域のヒープ上になるので、多くのオブジェクトが死んで生まれている、世代は、新しく作成されたオブジェクトは、特定の場所に、GCが最初に回復するために "死んだ "オブジェクト領域のこの部分を格納する場合、ので、それは多くのスペースを解放します。GCが最初にリサイクルされる領域に "死にかけ "オブジェクトを格納するとき、これは多くのスペースを解放します。

メモリ割り当て戦略の概要

  • もしそのオブジェクトがエデンで生まれ、最初のマイナーGC以降も生存しており、サバイバーに収容可能であれば、そのオブジェクトはサバイバーのスペースに移動され、オブジェクトの年齢が1に設定されます。年齢
    • オブジェクトが古い年齢に昇格する年齢のしきい値は、オプション ** -XX: MaxTenuringThreshold** で設定することができます。
  • 各年齢層への割り当ての原則は以下の通りです:
    • エデンへの優先割り当て
    • 大型オブジェは直接オールドエイジに割り当て
      • プログラム内に大きなオブジェクトが多すぎないようにします。
    • 長期生存オブジェクトはオールドエイジに割り当て
    • 動的オブジェクト年齢判定
      • Survivor領域にある同じ年齢のオブジェクトの合計がSurvivor領域の半分より大きい場合、その年齢以上の年齢のオブジェクトは直接オールドエイジに行くことができます。MaxTenuringThreshold で要求される年齢を待つ必要はありません。

スペース割り当ての保証メカニズム

なぜそうなるのかを簡単に説明すると、allocation2 にメモリを割り当てる際、eden 領域はほぼ割り当てられており、先ほども言ったように、eden 領域に割り当てに十分な領域がない場合、仮想マシンは Minor GC を開始します。 GC 期間中、仮想マシンは allocation1 を Survivor 領域に預けることができないこともわかります。GC 期間中、仮想マシンは allocation1 が Survivor 領域に格納できないことを発見し、割 り当て保証メカニズムによって新世代のオブジェクトを事前に旧世代に転送する必要があり、 旧世代には allocation1 を格納するのに十分な領域があるため、Full GC は行われません。Minor GC の実行後も、Minor GC の後に割り当てられたオブジェクトは、eden 領域に存在できる場合は、eden 領域に割り当てられます。このことは、以下のコードを実行することで確認できます:

public class GCTest {
 public static void main(String[] args) {
 byte[] allocation1, allocation2,allocation3,allocation4,allocation5;
 allocation1 = new byte[32000*1024];
 allocation2 = new byte[1000*1024];
 allocation3 = new byte[1000*1024];
 allocation4 = new byte[1000*1024];
 allocation5 = new byte[1000*1024];
 }
}

VI. TLAB

なぜTLABなのか

  • ヒープ領域はスレッド共有領域であり、どのスレッドもヒープ領域の共有データにアクセスできることはよく知られています。JVMではオブジェクト・インスタンスの作成が非常に頻繁に行われるため、同時実行環境でヒープからメモリ空間をパーティション分割するのはスレッドセーフではありません。
  • 一般的に、同じアドレスで複数のスレッドが動作するのを避けるために、ロックなどのメカニズムを使用する必要があり、その結果、割り当ての速度に影響します。
  • この問題を解決するためにTLABが作られました。

TLABとは

  • ゴミ収集の観点ではなく、メモリ・モデルの観点からエデン領域の分割を続けると、JVMは各スレッドにプライベート・キャッシュ領域を割り当てます。
  • 複数のスレッドが同時にメモリ割り当てを行う場合、TLABを使用することで、一連のスレッド安全性以外の問題を回避でき、メモリ割り当てのスループットも向上するため、このメモリ割り当て方法を高速割り当て戦略と呼ぶことができます。
  • すべてのOpenJDK派生JVMは、TLAB設計を提供します。

TLAB

  • すべてのオブジェクト・インスタンスがTLABでうまく割り当てられるわけではありませんが、JVは明示的にTLABをメモリ割り当ての最初の選択肢として使用しています。
  • アプリケーションでは、開発者がオプション"-XX:UseTLAB "でTLABスペースを有効にするかどうかを設定できます。
  • デフォルトでは、TLABスペースはEdenスペース全体の1%しか占めない非常に小さなメモリ量ですが、もちろん"-XX:TLABWasteTargetPercent "オプションで、TLABスペースが占めるEdenスペースの割合のサイズを設定することができます。
  • オブジェクトがTLAB空間でのメモリ確保に失敗するとすぐに、JVMはエデン空間で直接メモリを確保するロック・メカニズムを使用して、データ操作のアトミック性を確保しようとします。

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

VII.ヒープ空間のパラメータ化

-XX:PrintFlagsInitial: すべてのパラメータのデフォルト初期値を参照します。

-XX:PrintFlagsFinal: すべてのパラメータの最終値を参照します。

  • 特定のパラメータのコマンドを表示します:
    • jps: 現在実行中のプロセスを表示
    • jinfo -flag SurvivorRatio プロセス ID: 新世代の Eden と S0/S1 スペースの比率を表示します。

-Xms: ヒープ空間の初期メモリ

-Xmx: 最大ヒープ空間メモリ

-Xmn: 新世代のサイズを設定

-XX:NewRatio: ヒープ構造における新世代と旧世代の比率を設定します。

-XX:SurvivorRatio: 新世代における Eden と S0/S1 スペースの比率を設定します。

-XX:MaxTenuringThreshold: 新世代のゴミの最大年齢を設定します。

-XX:+PrintGCDetails: 詳細な GC 処理ログを出力します。

  • GCの簡単な情報を印刷します:
    • -XX:+PrintGC
    • -verbose:gc

-XX:HandlePromotionFailure:スペース割り当て保証を設定するかどうか

HandlePromotionFailure パラメータ 説明Minor Gc の場合、VM は旧世代で利用可能な最大連続スペースが、新世代の全オブジェクトの合計スペースより大きいかどうかをチェックします。

  • より大きい場合、このマイナーGCは安全です。
  • 未満の場合、VMは-XX:HandlePromotionFailure設定値が保証を失敗させるかどうかを確認します。
    • HandlePromotionFailure=trueの場合、旧エージで利用可能な最大連続領域が、旧エージに昇格したオブジェクトの平均サイズよりも大きいかどうかをチェックし続けます。
      • それよりも大きい場合、割り当て保証をトリガできるかどうかを確認するためにマイナーGCが試みられますが、このマイナーGCはまだ危険です;
      • それ以下の場合は、代わりにFull GCが実行されます。
    • HandlePromotionFailure=falseの場合、代わりにFull GCを実行します。

注:JDK6 Update24後、HandlePromotionFailureパラメータは、もはや仮想マシンの空間割り当て保証ポリシーに影響を与えません。 openJDKのソースコードの変更を観察すると、HandlePromotionFailureパラメータはまだソースコードで定義されていますが、それはもはやコードで使用されていません。ルールが変更された後のJDK6 Update24:連続空間の古い世代がオブジェクトの新しい世代の合計サイズまたは連続したプロモーションの平均サイズよりも大きい限り、マイナーGCが実施され、そうでない場合はフルGCが実施されます。

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

Javaのヒープメモリ上の "Java仮想マシンの深い理解 "では、この説明を持って:JITコンパイル期間とエスケープ解析技術の漸進的な成熟の発展に伴い、==スタック割り当て、スカラー置換最適化技術==いくつかの微妙な変化につながる、すべてのオブジェクトは、ヒープに割り当てられている徐々に少なくなってきている "絶対に".

Java 仮想マシンでは、オブジェクトは Java ヒープにメモリが割り当てられることが常識です。しかし、特殊なケースとして、オブジェクトがスタックに割り当てられるように最適化される場合があります。これにより、ヒープ上にメモリを確保する必要がなくなり、ゴミ収集の必要もなくなります。これは最も一般的なオフヒープ・ストレージ手法です。

また、OpenJDK TaoBaoVMは、革新的なGCIH(GCinvisibleヒープ)技術オフヒープ、ヒープからヒープへのJavaオブジェクトの長いライフサイクルを達成するために、GCは、GCIH内部のJavaオブジェクトを管理することはできませんに基づいてカスタマイズの前に述べた深さは、GCの回収頻度を削減し、GCの回収効率を向上させる目的を達成するために。リサイクルの頻度とGCのリサイクル効率を向上させます。

脱出分析

  • ヒープ上のオブジェクトがどのようにスタックに割り当てられるかは、エスケープ分析を使う必要があります。
  • これは、Javaプログラムにおける同期負荷とメモリヒープ割り当て圧力を効果的に削減できる、機能横断的なグローバルデータフロー解析アルゴリズムです。
  • エスケープ分析によって、Java Hotspotコンパイラは新しいオブジェクトへの参照の使用範囲を分析し、オブジェクトをヒープに割り当てるかどうかを決定することができます。
  • エスケープ分析の基本動作は、オブジェクトの動的スコープを分析することです:
    • オブジェクトがメソッド内で定義され、そのオブジェクトがメソッド内でのみ使用される場合、エスケープは発生しないとみなされます
    • オブジェクトがメソッド内で定義され、外部のメソッドから参照される場合、エスケープが発生したとみなされます。例えば、呼び出しパラメータとして他の場所に渡される場合などです。
  • エスケープ解析が起こったかどうかを判断する簡単な方法は、newのオブジェクト実体がメソッドの外で呼び出された可能性を見ることです
コード解析
1.
public void method(){
 V v = new V();
 //use V
 //......
 v = null;
}

エスケープが発生していないオブジェクトをスタックに割り当てることができるのは、以下の理由によります:

1.メソッドの実行が終了すると、スタック領域が削除されます。

2.VMのスタックスペースはスレッドプライベートであり、共有されません。

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

上記のメソッドで返される sb はメソッドの外で使われているため、エスケープが発生します。もし StringBuffer sb をメソッドからエスケープさせたくない場合は、上記のコードはこのように書くことができます:

public static String createStringBuffer(String s1,String s2){
 StringBuffer sb = new StringBuffer();
 sb.append(s1);
 sb.append(s2);
 return sb.toString();
}
3.
/**
 * 脱出分析
 *
 * エスケープ解析が起こったかどうかを素早く判断するには、newオブジェクトの実体を見ればいい。”メソッドの外で呼び出される可能性があるかどうか。
 */
public class EscapeAnalysis {
 public EscapeAnalysis obj;
 /*
 このメソッドは、エスケープが発生したEscapeAnalysisオブジェクトを返す。
 */
 public EscapeAnalysis getInstance(){
 return obj == null? new EscapeAnalysis() : obj;
 }
 /*
 メンバー属性への値の割り当て、エスケープが起こる
 */
 public void setObj(){
 this.obj = new EscapeAnalysis();
 }
 //考えてみよう:もし現在のobj参照がstaticと宣言されていたらどうだろう?エスケープはまだ起こるだろう。
 /*
 オブジェクトのスコープは現在のメソッドに対してのみ有効であり、エスケープは起こらない。
 */
 public void useEscapeAnalysis(){
 EscapeAnalysis e = new EscapeAnalysis();
 }
 /*
 をエスケープしたメンバ変数の値を参照する。
 */
 public void useEscapeAnalysis1(){
 EscapeAnalysis e = getInstance();
 //getInstance().xxx()脱走もまた起こる
 }
}
4.パラメータ設定
  • JDK バージョン 6u23 以降、HotSpot ではエスケープ解析がデフォルトで有効になっています。
  • それ以前のバージョンを使用している場合、開発者は
    • -XX:DoEscapeAnalysisで明示的にエスケープ解析を有効にします。
    • -XX:+PrintEscapeAnalysisでエスケープ解析のフィルタリング結果を見ることができます。

結論:開発でローカル変数を使えるのであれば、メソッドの外では使わないようにしましょう。

コード最適化理論

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

  • スタックへの割り当て:ヒープ割り当てからスタック割り当てへの変換。オブジェクトがサブスレッドで割り当てられる場合、オブジェクトへのポインタが決してエスケープされないようにするために、オブジェクトはヒープ割り当てではなくスタック割り当ての候補になることがあります。
  • 同期の省略:オブジェクトが1つのスレッドからのみアクセス可能であることが判明した場合、そのオブジェクトに対する操作は同期を考慮せずに実行できます。
  • オブジェクトの分離またはスカラー置換:連続したメモリ構造として存在する必要がないかもしれないオブジェクトも北に求めることができ、その後、オブジェクトの一部は、ヒープメモリではなく、CPUのレジスタに格納することができます。
スタックへの割り当て
  • JIT コンパイラは、メソッドからエスケープしていないオブジェクトがスタックに割り当てられる可能性があることを明らかにするエスケープ分析の結果に基づいて、コンパイル時にスタックへの割り当てを最適化することができます。割り当てが完了すると、呼び出しスタック上で実行が続行され、最終的にスレッドが終了し、スタック領域が取り戻され、ローカル変数オブジェクトが取り戻されます。これにより、マシン型のゴミ収集が不要になります。
  • エスケープが発生する一般的なシナリオ:メンバ変数への値の代入、メソッドの戻り値、インスタンス参照の受け渡し

コード解析:

エスケープ解析をオフにした次のコードでは、10,000,000 個のオブジェクトが維持されます。

/**
 * スタックテストでの割り当て
 * -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
 */
public class StackAllocation {
 public static void main(String[] args) {
 long start = System.currentTimeMillis();
 for (int i = 0; i < 10000000; i++) {
 alloc();
 }
 // 実行時間を見る
 long end = System.currentTimeMillis();
 System.out.println("費やされた時間は次のとおりである。 + (end - start) + " ms");
 // ヒープ・メモリ内のオブジェクト数を見るために、スレッドはスリープする。
 try {
 Thread.sleep(1000000);
 } catch (InterruptedException e1) {
 e1.printStackTrace();
 }
 }
 private static void alloc() {
 User user = new User();//脱走は起きていない
 }
 static class User {
 }
}
同期省略
  • スレッドの同期は非常にコストがかかり、同期の結果、同時実行性とパフォーマンスが低下します。
  • 同期ブロックを動的にコンパイルする場合、JITコンパイラはエスケープ解析を使用して、同期ブロックで使用されるロック・オブジェクトが1つのスレッドによってのみアクセス可能で、他のスレッドに解放されていないかどうかを判断することができます。そうでない場合、JITコンパイラはブロックをコンパイルする際にコードのこの部分を非同期化します。これにより、同時実行性とパフォーマンスが大幅に向上します。この非同期化のプロセスは、同期省略(synchronisation omission)またはロック除去(lock elimination)と呼ばれます。
/**
 * 同期省略
 */
public class SynchronizedTest {
 public void f() {
 Object hollis = new Object();
 synchronized(hollis) {
 System.out.println(hollis);
 }
 }
 //コードはホリス・オブジェクトをロックしているが、ホリス・オブジェクトのライフサイクルはf()メソッドの中だけである。
 //MinorGCは他のスレッドからアクセス制御されないため、JITコンパイル段階で最適化される。
 //に最適化されている。
 public void f2() {
 Object hollis = new Object();
 System.out.println(hollis);
 }
}

注:上記のコード例は、同期省略というコード最適化テクニックを視覚的に説明するためのものです。コード自体には問題があります。以下のようになります。

public class SynchronizedTest {
 public void f() {
 Object hollis = new Object();
 synchronized(hollis) {
 System.out.println(hollis);
 }
 }
}

同期コードでは、異なるスレッド間で同期を共有するためにロックを追加する必要があります。しかし、追加されるロックが同じロックでなければ意味がありません。 上記の同期コード・ブロックでは、ロック・オブジェクトhollosが毎回新しく生成されますが、これはロックを追加することによる同期効果に沿ったものではありません。これは間違いなくバギーなコードです。しかし、これは問題を示しています!

分離オブジェクトまたはスカラー置換
  • スカラー(scalar):小さなデータに分解できないデータのこと。Javaの本来のデータ型はスカラーです。
  • これに対して、まだ分解可能なデータはアグリゲートと呼ばれ、Javaのオブジェクトは、他のアグリゲートやスカラーに分解できるので、アグリゲートです。
  • JITフェーズでは、エスケープ解析の結果、オブジェクトが外部からアクセスされないことが判明した場合、JIT最適化の後、オブジェクトは、置換されるメンバ変数を含む数に分解されます。この処理がスカラー置換です。
1.コード分析1
public class ScalarTest {
 public static void main(String[] args) {
 alloc(); 
 }
 public static void alloc(){
 Point point = new Point(1,2);
 }
}
class Point{
 private int x;
 private int y;
 public Point(int x,int y){
 this.x = x;
 this.y = y;
 }
}

上記のコードにスカラーを代入すると、次のようになります。

public static void alloc(){
 int x = 1;
 int y = 2;
}

ご覧の通り、逃走分析で逃走しなかったことが判明した後、集計量であるポイントは2つのスカラーに置き換えられました。では、スカラー置換の利点は何でしょうか?それは、ヒープメモリの占有率を大幅に削減できることです。一度オブジェクトを作成する必要がないため、ヒープメモリを確保する必要がなくなります。スカラー置換は、スタック上の割り当てのための良い基盤を提供します。

設定パラメータ: -XX:+EliminateAllocations スカラー置換を有効にします。

2.サンプルコード 2:
/**
 * スカラー置換テスト
 * -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
 */
public class ScalarReplace {
 public static class User {
 public int id;//スカラー
 public String name;//集計
 }
 public static void alloc() {
 User u = new User();//脱走は起きていない
 u.id = 5;
 u.name = "www.atguigu.com";
 }
 public static void main(String[] args) {
 long start = System.currentTimeMillis();
 for (int i = 0; i < 10000000; i++) {
 alloc();
 }
 long end = System.currentTimeMillis();
 System.out.println("費やされた時間は次のとおりである。 + (end - start) + " ms");
 }
}

テストして比較することができますし、スカラー置換をオンにした場合のパフォーマンス向上はやはり大きいです。

脱出分析のまとめ

  • エスケープ分析に関する論文は1999年に発表されましたが、実装されたのはJDK 1.6からで、現在ではこの技術はあまり成熟していません。
  • その根本的な理由は、エスケープ解析の性能消費が彼の消費よりも高くなるという保証がないからです。エスケープ分析は、スカラー置換、スタック上の割り当て、およびロック除去を行うことができますが。しかし、エスケープ分析自体も一連の複雑な分析を実行する必要があり、実際には比較的時間のかかるプロセスです。
  • 極端な例としては、脱出分析の結果、脱出しないオブジェクトが存在しないことが判明した場合です。それでは、この脱出分析のプロセスは無駄になってしまいます。
  • この技法はあまり成熟していませんが、オンザフライ・コンパイラの最適化技法においても非常に重要なツールです。
  • エスケープ分析では、JVMがエスケープしないオブジェクトをスタック上に割り当てるという議論があることが指摘されています。これは理論的には可能ですが、JVM設計者の選択によります。私の知る限り、エスケープ分析に関連するドキュメントに記載されているように、Oracle HotspotJVMではこのようなことは行われませんので、すべてのオブジェクトインスタンスがヒープ上に作成されることは明らかです。この記事の質問に戻ります:オブジェクトを割り当てるための唯一の選択肢はヒープなのでしょうか?最初は否定的な態度でしたが、今はこの見解を肯定するつもりです。これは矛盾していませんか。ビューのいわゆる否定的な点として、この記事を通じて、少なくともオブジェクトの分離やこの点のスカラー置換はまだ存在し、実行中のコードの効率を最適化し、ヒープ空間の占有を削減し、スタック上の割り当てのアイデアを反映して、まだ進歩があります。
  • JDKは大きく変わり、インターン文字列キャッシュと静的変数は、以前はパーマネント世代に割り当てられていましたが、現在はメタデータ領域に置き換えられています。しかし、インターン文字列キャッシュと静的変数はメタデータ領域に移動する代わりに、ヒープ上に直接割り当てられています。
  • ヤング・ジェネレーションとは、モノの誕生、統治、終焉の領域であり、そこでモノは創造され、適用され、最終的にゴミ収集人によって回収され、その生涯を終えます。
  • 古い世代は、通常サバイバー領域からJavaオブジェクトのコピーをフィルタリングするために、長いライフサイクルのオブジェクトを防ぐために。もちろん、特殊なケースがありますが、通常のオブジェクトは、オブジェクトが大きい場合、JVMはエデンの他の場所に直接割り当てしようとする、TLABに割り当てられることを知って、彼はヒットしたオブジェクトは、完全に新しい世代の十分な長さの連続空き領域を見つけることができない場合、JVMは直接古い世代に割り当てられます。
  • 一般に、MajorGCよりもMinorGCの方が発生頻度が高く、すなわち、ゴミの回収は若い世代よりも古い世代の方が発生頻度が低い。
Read next

デザインパターンの注意点

1. デザイン・パターンの目的 2. デザイン・パターンの7つの原則 クラスの場合、クラスは1つの義務だけを担当します。例えば、クラスAは1つの義務だけを担当します。クラスAが複数の原則を担当する必要がある場合、これを行う最善の方法は、クラスAをより細かいA1,A2に分割することです。細部の可変性に比べて、抽象化ははるかに安定しており、抽象化に基づくアーキテクチャは、細部に基づくアーキテクチャよりもはるかに安定しています。

Jun 28, 2020 · 15 min read