Javaのガベージコレクション機構を詳しく解説 - 侵入から発掘まで、覚えられないなら殺しに来い!

記事ディレクトリ

まず第一に、どのメモリを回復する必要があるのか​​を知る必要があります。

どのメモリを再利用する必要があるか

Java メモリ ランタイム領域のさまざまな部分のうち、ヒープ領域とメソッド領域の 2 つの領域には大きな不確実性があり、インターフェイスの複数の実装クラスで必要なメモリが異なる可能性があり、メソッドの実行が異なる可能性があります。条件分岐に必要なメモリも異なる場合があります。プログラムがどのオブジェクトを作成するか、およびオブジェクトの数を知ることができるのは実行時のみです。メモリのこの部分の割り当てと回復は動的です。

ガベージ コレクターが注目するのは、ヒープ領域とメソッド領域のメモリをどのように管理するかということであり、通常、メモリの割り当てと回復と呼ばれるのは、メモリのこの部分のみを指します。

コレクション ヒープ: ガベージの定義

参照カウントアルゴリズム:

参照カウンターをオブジェクトに追加します。

  • それへの参照があるたびに、カウンター値は 1 ずつ増加します。
  • 参照が無効になると、カウンタ値は 1 つ減ります。

カウンタが常にゼロになっているオブジェクトは使用できなくなります

しかし、Java の分野では、少なくとも主流の Java 仮想マシンではメモリ管理に参照カウントアルゴリズムが使用されておらず、例えば、単純な参照カウントではオブジェクト間の循環参照の問題を解決することが困難です

簡単な例を挙げると、オブジェクト objA と objB は両方ともフィールド インスタンスを持ち、代入により
objA が作成されます。再度アクセスすることはできませんが、相互に参照しているため、参照カウントは 0 ではなく、参照カウントはアルゴリズムはそれらをリサイクルできません。

到達可能性分析アルゴリズム:

現在主流の商用プログラミング言語のメモリ管理サブシステムは、到達可能性分析アルゴリズムを使用して、オブジェクトが生きているかどうかを判断します。

このアルゴリズムの基本的な考え方は、「GC ルート」と呼ばれる一連のルート オブジェクトを開始ノード セットとして使用することです。これらのノードから、参照関係に従って下方向に検索します。検索プロセスによってたどられるパスは「参照」と呼ばれます。チェーン」(参照チェーン)、オブジェクトと GC ルートの間に参照リンクがない場合、またはグラフ理論の観点から言えば、オブジェクトが GC ルートから到達できない場合、そのオブジェクトはもう使用できないことが証明されます。

以下の図に示すように、オブジェクト 5、オブジェクト 6、およびオブジェクト 7 は相互に関連していますが、GC ルートに到達できないため、リサイクル可能なオブジェクトとして判断されます。

ここに画像の説明を挿入

GC ルートのオブジェクト

Java テクノロジー システムでは、GC ルートとして修正できるオブジェクトには次のものがあります。

  • 仮想マシンスタック(スタックフレーム内のローカル変数テーブル)で参照されるオブジェクト呼び出される各スレッドのメソッド スタックで使用されるパラメータ、ローカル変数、一時変数など。
  • メソッド領域のクラス静的プロパティによって参照されるオブジェクト、Java クラスの参照型静的変数など。
  • メソッド領域の定数によって参照されるオブジェクト、文字列定数プール (文字列テーブル) 内の参照など。
  • ローカル メソッド スタック内の JNI によって参照されるオブジェクト (一般にネイティブ メソッドとして知られています)
  • Java仮想マシン内の参照これには、基本データ型に対応する Class オブジェクト一部の常駐例外オブジェクト(NullPointExcepiton、OutOfMemoryError など)、およびシステム クラス ローダーなどがあります。
  • 同期ロック (synchronized キーワード) によって保持されているすべてのオブジェクト
  • Java仮想マシンの内部状況を反映するJMXBean、JVMTIに登録されるコールバック、ローカルコードキャッシュ待って。

リサイクル方法分野:ごみの定義

メソッド領域のガベージ コレクションでは、主に次の 2 つの部分がリサイクルされます。使用されなくなった廃止された定数と型

廃止された定数のリサイクルは、Java ヒープ内のオブジェクトの再利用と非常に似ています。

定数プールでのリテラルのリサイクルの例を示します。
文字列「java」が定数プールに入ったが、現在のシステムには値が「java」である文字列オブジェクトが存在しない場合、つまり、文字列オブジェクトが存在しない場合は定数プール内の「java」定数を参照しており、仮想マシン内にはこのリテラルへの他の参照はありません。この時点でメモリ再利用が発生し、ガベージ コレクターが必要と判断した場合、「java」定数はシステムによって定数プールから削除されます。定数プール内の他のクラス (インターフェイス)、メソッド、およびフィールドのシンボリック参照も同様です。

定数が「廃止された」かどうかを判断するのは比較的簡単です。この定数への参照があるかどうかを確認することは問題ありませんが、型が「使用されなくなったクラス」に属しているかどうかを判断するための条件はより厳しくなります。 。

次の 3 つの条件を同時に満たす必要があります。

  • このクラスのすべてのインスタンスはリサイクルされています。つまり、Java ヒープにはこのクラスのインスタンスと派生サブクラスは存在しません。
  • このクラスをロードしたクラスローダーはリサイクルされましたこの条件は、OSGi、JSP のリロードなどのクラス ローダー シナリオを置き換えるように慎重に設計されていない限り、通常は達成することが困難です。
  • このクラスに対応する java.lang.Class オブジェクトはどこからも参照されず、このクラスのメソッドにはリフレクションを通じてアクセスできません。

ゴミをリサイクルする方法

世代別コレクション理論とガベージ コレクション アルゴリズムの演繹については、私の別のブログ投稿を参照してください: https://yangyongli.blog.csdn.net/article/details/126473167

上記の記事で gc が開発され続けた後、徐々に現在使用している gc が完成しました。

ガベージ コレクション アルゴリズムの概要

マーククリアアルゴリズム (旧世代にも適用可能ですが、基本的に廃止されました)

これは最も初期の最も基本的なアルゴリズムであり、最初にリサイクル対象のオブジェクトにマークを付け、マーク付けが完了した後、マークされたオブジェクトは一律にリサイクルされます。

デメリット:
1. 実行効率が不安定 オブジェクト数が増えるとマーキングの数も増え、クリア回数も増えて実行効率が低下する 2. メモリ空間が断片化するため、クリアして予約した場合オブジェクトメモリがインターリーブされると、メモリ空間が断片化され、十分な空間があっても断片化により新しいオブジェクトを作成できなくなり、無駄な空間が発生します。

マークコピーアルゴリズム (現在新世代で一般的に使用されています)

その実装原理は、メモリ空間を 2 つの空間に分割し、毎回一方のみを使用し、残ったオブジェクトをもう一方の空間にコピーすることであり、多数のオブジェクトがリサイクルされる場合、コピーする必要があるのはごく一部だけであるため、新入生世代に最適です。(高速かつ高効率)

短所: コストがかかるのは明らかで、スペースの半分を無駄にする必要があります。

ただし、生成消滅するオブジェクトの特性に応じて、より優れた半領域複製戦略、つまり「アペル」スタイルのリサイクルが後に導き出されます。具体的な戦略は、新世代空間をより大きなエデン空間と 2 つのエデン空間に分割することです。 HotSpot を含む小さい Survivor スペース 仮想マシンのデフォルトの比率は Eden: Survivor=8:1 です。これは、新しい世代ごとにスペースの 90% (Survivor80+Eden10) が割り当てられることを意味し、スペースの無駄が大幅に削減されます。このような問題も発生します。合計が 100MB のスペースである場合、このリサイクル対象は 20MB です。当然、残りの 10MB を占有することはできません。このような状況が起こらないとは誰も保証できません。このとき、Appel は「エスケープ」を採用しました。 「ドア」設計 - メモリ割り当ての保証

メモリ割り当て保証: Survivor が耐えられない部分を旧世代に直接入れることです。これは、上の例の 20MB を古い世代に直接配置することと同じです (つまり、古い世代を昇格する特殊なケース)

マーキング照合アルゴリズム (現在は旧世代で一般的に使用されています)

マーククリアとは異なり、生き残ったオブジェクトがマークされた後、直接のクリアは行われませんが、生き残ったオブジェクトは一方の端で一緒にソートされ、スペースの断片化の問題が解決されます。

旧世代の特性上、一部しかクリアされないため、旧世代では多数の生きたオブジェクトが存在し、オブジェクトインデックスの更新が必要となり、システムに負荷がかかり、確実にこのようなオブジェクトに対する操作は、システムが停止しているときに実行する必要があります。これが、私たちがよく聞く「Stop The World」です。

また、泥を一歩ずつ進める別の方法として、通常時はマークアンドクリア方式を使用し、断片化が発生してからオブジェクトの割り当てに影響が出るまでマークアンドクリア方式を使用するという方法もあります。完全な空間を得るために。この処理方法は、マーククリア アルゴリズムに基づいて CMS コレクターによって採用されています


JVM がどのようにリサイクルするかを見てみましょう。JVM には 3 種類の GC があります。

JVM GCの種類

JVM の共通 GC には次の 3 つのタイプがあります。マイナー GC、メジャー GC およびフル GC

HotSpot VM の実装では、その中の GC はリカバリ領域に応じて 2 つのタイプに分けられます。

  • 1 つは部分コレクション (Partial GC)
  • 1 つはフル ヒープ コレクション (フル GC)

部分コレクション (部分 GC): Java ヒープ全体を完全に収集するガベージ コレクションではありません。Java ヒープは次のように分割されます。

  • 新世代コレクション (マイナー GC/ヤング GC): 新世代のガベージ コレクションのみ
  • 古い世代のコレクション (メジャー GC/古い GC): 古い世代のガベージ コレクションのみ
  • 混合コレクション (Mixed GC): 新しい世代全体と古い世代の一部のガベージ コレクションを収集します。現在、この動作を持つのは G1 GC のみです。

ヒープ全体のコレクション (フル GC): Javaヒープおよびメソッド領域全体のガベージコレクション

注: JVM が GC を実行しているとき、すべての領域 (新しい世代、古い世代、メソッド領域) が毎回一緒にリサイクルされるわけではありません。ほとんどの場合、リカバリは新しい世代を参照します。

GCトリガー機構

若い世代の GC (マイナー GC) トリガーメカニズム

トリガーメカニズム:

  • 若い世代の領域が不足すると、マイナー GC がトリガーされますここで若い世代のスペースが不足しているということは、Eden エリアが満杯であり、Survivor エリアが満杯の場合、Survivor エリアは GC をトリガーしないことを意味します。(すべてのマイナー GC は、若い世代の記憶をクリーンアップします)

ほとんどの Java オブジェクトには生と死の特性があるため、マイナー GC が非常に頻繁に発生し、回復速度が一般に速くなります。マイナー GC により
STW (stop to world) が発生し、他のユーザー スレッドが中断され、ガベージ コレクションが完了するまで待機します。ユーザースレッドの実行が再開される前に

旧世代 GC (メジャー GC/フル GC) トリガーメカニズム

古い時代に発生するGCのことを指し、オブジェクトが古い時代に消滅することを「Major GCまたはFull GC」といいますが、

通常、Major GC が表示されますが、多くの場合、少なくとも 1 つのマイナー GC が伴います。(ただし、絶対ではありません。Parallel Scavenge コレクターの収集戦略には、Major GC を直接選択する戦略があります)

トリガーメカニズム:

  • つまり、古い世代に十分なスペースがない場合は、最初にマイナー GC をトリガーしようとし、後で十分なスペースがない場合はメジャー GC をトリガーします。

PS:
メジャー GC の速度は一般的にマイナー GC より 10 倍以上遅く、STW 時間は長くなります。メジャー
GC 後にメモリが十分でない場合、OOM が報告されます。
メジャー GC の速度は一般に 10 倍以上ですマイナー GC よりも 1 倍遅いです。

完全な GC トリガー メカニズム

フル GC の実行をトリガーする状況は 5 つあります。

  • System.gc() を呼び出すと、システムはフル GC の実行を推奨しますが、必ずしもフル GC を実行する必要はありません。
  • 旧世代ではスペースが不足
  • メソッド領域のスペースが不十分です
  • マイナー GC の後、古い世代の平均サイズが古い世代の利用可能なメモリより大きくなります
  • Eden領域からコピーする際、およびto領域からto領域にコピーする場合、オブジェクトのサイズがto領域の空きメモリよりも大きい場合、オブジェクトは旧世代に転送され、旧世代の空きメモリはeden領域の空きメモリよりも小さくなります。オブジェクトのサイズ(GC 後に十分でない場合はどうすればよいでしょうか?その場合は言うまでもなく、OOM 例外が送信されます)

さらに、特別な注意を払ってください。開発またはチューニングではフル GC を可能な限り回避する必要があります。

Java ヒープを複数の世代に分割する必要があるのはなぜですか?

調査の結果、オブジェクトごとにライフサイクルが異なり、オブジェクトの 70% ~ 99% が一時オブジェクトであることがわかりました。

実際には、世代を分割しないことは完全に可能です。世代の唯一の理由は、GC パフォーマンスを最適化することです。世代がない場合、すべてのオブジェクトは 1 つの領域にあります。GC が必要な場合は、すべてのオブジェクトを走査する必要があります。 GC ユーザースレッドを一時停止するので、この場合パフォーマンスはかなり消費されますが、ほとんどのオブジェクトは生まれて死んでいきます。長く生き続けるオブジェクトを分割してみてはいかがでしょうか?死んだものを再利用すれば十分ですつまり、死にやすい領域は頻繁に再利用され、死ににくい領域はあまり再利用されません。

これは実際には世代リサイクル理論に基づいています。詳細については、私の他のブログ投稿を参照してください: https://blog.csdn.net/weixin_45525272/article/details/126473167

JVM における完全な GC プロセスとは何ですか?

新しく作成されたオブジェクトは通常、新世代に割り当てられますが、一般的に使用される新世代ガベージ コレクターは ParNew ガベージ コレクターであり、新世代を 8:1:1 に従って Eden 領域と 2 つの Survivor 領域に分割します。ある瞬間、私たちが作成したオブジェクトはエデンエリアを完全に埋め尽くします、そしてこのオブジェクトは新しい世代を埋める最後のオブジェクトです現時点では、マイナー GC がトリガーされる

公式のマイナー GC の前に、JVM はまず新しい世代のオブジェクトが古い世代の残りのスペースより大きいか小さいかをチェックします。

なぜそのようなチェックを行うのでしょうか?
理由は非常に簡単で、Minor GC後にSurvivor領域に残ったオブジェクトが保持できなくなった場合、それらのオブジェクトは旧世代に入ってしまうため、事前に旧世代で足りるかどうかを確認する必要があるからです。

したがって、次の 2 つの状況があります。

  1. 古い世代の残りの領域が新しい世代のオブジェクト サイズよりも大きい場合は、直接マイナー GC が実行され、GC の後、生き残ったものでは不十分で、古い世代で絶対に十分になります。

  2. 旧世代の残り領域が新世代のオブジェクトサイズより小さいため、このとき「旧世代領域割り当て保証ルール」が有効になっているか確認する必要があります。具体的には、-XX:-HandlePromotionFailure パラメーターが設定されているかどうかを確認します。

    古い世代のスペース割り当て保証ルールは次のとおりです。古い世代の残りのスペースが、前回のマイナー GC 後に残ったオブジェクトのサイズよりも大きい場合、マイナー GC が許可されます。
    なぜなら、確率的に言えば、前回を手放したら今回も手放すべきだからです。

  • 現時点では、保証ルールの割り当てには 2 つの状況もあります。
    • 古い世代の残りの領域のサイズが、前回のマイナー GC 後に残っているオブジェクトのサイズより大きいため、マイナー GC が実行されます。
    • 古い世代に残っている領域のサイズは、前回のマイナー GC 後に残っているオブジェクトのサイズより小さいです。フル GC を実行して古い世代を空にしてから確認してください

旧世代で領域割り当て保証ルールを有効にする可能性は非常に高いとしか言​​えません。マイナー GC 後に残ったオブジェクトは旧世代に配置できるだけなので、当然不測の事態は発生します。マイナー GC の後、次の 3 つの状況になります。

  1. マイナー GC 後のオブジェクトは Survivor エリアに配置するのに十分であり、全員が満足し、GC は終了します
  2. マイナー GC 後のオブジェクトは、Survivor 領域に配置するのに十分ではないため、古い世代に入り、古い世代を削除できます。それで問題ありません。GC は終了しました。
  3. マイナー GC 後のオブジェクトは Survivor 領域に配置するのに十分ではなく、古い世代は配置できないため、フル GC のみにすることができます

上記はすべて GC が成功した例であり、GC が失敗して OOM が報告される原因となる状況が他にも 3 つあります。

  1. 前のセクションのフル GC の直後、古い世代はまだ残りのオブジェクトを配置できないため、OOM しか実行できません。
  2. 古い世代の割り当て保証メカニズムが有効になっておらず、フル GC 後も古い世代は残りのオブジェクトを保持できず、OOM のみが可能です。
  3. 古い世代の割り当て保証メカニズムは有効になっていますが、保証は失敗します。フル GC の後、古い世代は依然として残りのオブジェクトを削除できず、OOM になる可能性もあります。

JVM GC に関する注意:

フル GC は何を引き起こすのでしょうか?

フル GC は「Stop The World」になります。つまり、ユーザーのアプリケーションは GC 期間中一時停止されます。したがって、開発するときは、フル GC をできるだけ少なくするようにしてください。

JVM はいつ GC をトリガーしますか?

上記のプロセスを要約します。つまり、
Eden 領域のスペースが使い果たされると、Java 仮想マシンはマイナー GC をトリガーして新しい世代のガベージを収集し、生き残ったオブジェクトは Survivor 領域に送信されます。 . 簡単に言えば、新世代のEden領域がいっぱいになるとマイナーGCがトリガーされる

シリアル GC では、古い世代の残りのメモリが、以前の若い世代から古い世代への昇格の平均サイズより小さい場合、フル GC が実行されます。CMS などのコンカレント コレクターでは、古い世代のメモリ使用量が時々チェックされ、一定の割合を超えた場合に Full GC リサイクルが実行されます。

FullGCの数を減らすにはどうすればよいですか?

フル GC の数を減らすには、次の措置を講じることができます。

  1. メソッド領域のスペースを増やします。
  2. 旧世代のスペースを増やします。
  3. 新しい世代のスペースを削減します。
  4. System.gc() メソッドは禁止されています。
  5. マークソートアルゴリズムを使用して、可能な限り大きな連続メモリスペースを確保します。
  6. コード内の役に立たない大きなオブジェクトのトラブルシューティングを行います。

なぜマークを使用して古い世代をコピーできないのですか?

旧世代で保持されているオブジェクトは消滅しにくく、マークコピーアルゴリズムはオブジェクト生存率が高いとコピー回数が多くなり効率が低下するため、このアルゴリズムをそのまま旧世代で使用することはできません。

なぜ新世代はエデンとサバイバーに分かれるのか?

現在の商用 Java 仮想マシンのほとんどは、最初に「マークコピー アルゴリズム」を使用して新しい世代をリサイクルしていますが、このアルゴリズムは初期のガベージ コレクションに「ハーフリージョン コピー」メカニズムを使用していました。利用可能なメモリを容量に応じて同じサイズの 2 つの部分に分割し、一度にそのうちの 1 つだけを使用します。このブロックのメモリがなくなったら、残ったオブジェクトを別のブロックにコピーし、使用済みのメモリ空間を一度にクリーンアップします。これは実装が簡単で実行が効率的ですが、欠点も明らかです。このコピー回復アルゴリズムの代償として、使用可能なメモリが元のメモリの半分に減り、少しスペースの無駄が多すぎます。

Survivor リージョンが 2 つあるのはなぜですか?

Survivor 領域を 2 つ設定する最大の利点は、メモリの断片化を解決できることです。

まず、Survivor にはエリアが 1 つだけあると仮定します。マイナー GC の実行後、Eden 領域はクリアされ、生き残ったオブジェクトは Survivor 領域に配置されます。以前の生存者エリアにある一部のオブジェクトもクリアする必要がある場合があります問題は、現時点でそれらをどのようにクリアするかです。

このシナリオでは、クリアとマークすることしかできませんマーキングとクリアの最大の問題はメモリの断片化であることがわかっています。新世代のように頻繁に使用できなくなる領域では、マーククリアを使用すると、必然的にメモリの深刻な断片化が発生します。

ただし、ここでは Survivor 領域を 2 つ設定します。Survivor には 2 つの領域があるため、各マイナー GC によって Eden 領域と From 領域に残っているオブジェクトが To 領域にコピーされ、Eden 領域と From 領域はクリアされます。2 回目のマイナー GC では、From と To の責任が交換され、Eden 領域と To 領域に残っているオブジェクトは From 領域にコピーされます。

このメカニズムの最大の利点は、プロセス全体を通じて、1 つの Survivor スペースが常に空であり、もう 1 つの空ではない Survivor スペースにはフラグメントがないことです。では、なぜサバイバーはさらに多くのブロックに分割しないのでしょうか? たとえば、3 つに分割するか、4 つまたは 5 つに分割するか? もちろん、Survivor エリアをさらに細分化すると、各ブロックのスペースが比較的小さくなり、Survivor エリアが埋まりやすくなります。重量を測った後、2 つの生存者エリアが最適な解決策となる可能性があります

新世代と旧世代で異なるリサイクル アルゴリズムが使用されるのはなぜですか?

エリア内のほとんどのオブジェクトが死にかけていて、ガベージ コレクション プロセスで生き残るのが難しい場合は、それらをまとめて、生き残る多数のオブジェクトにマークを付けるのではなく、毎回少数の生き残ったオブジェクトを維持する方法だけに焦点を当てます。回収すれば、比較的低コストで大量のスペースを回収できます。

残りが消滅しにくいオブジェクトである場合は、それらをまとめます。これにより、仮想マシンはガベージ コレクションの時間オーバーヘッドとメモリ領域の有効利用の両方を考慮して、より低い頻度でこの領域を再利用できるようになります。

新しい世代は最初の状況と一致しているため、マーク コピー アルゴリズムを使用すると、大量のガベージをより適切に削除し、少数の生き残ったオブジェクトを保持できますが、古い世代では、少数のオブジェクトのみがリサイクルされます。スペースの断片化の問題を回避するには、照合アルゴリズムが最適であることをマークします。

おすすめ

転載: blog.csdn.net/weixin_45525272/article/details/126469398