Java仮想マシンの深い理解(第2版)学習2:ガベージコレクションアルゴリズム

オブジェクトサバイバルジャッジメント

仮想マシンは、ヒープを再利用する前に、これらのオブジェクトのどれが「生きている」かを判断する必要があります。

参照カウント

原則:オブジェクトに参照カウンターを追加します。参照する場所がある場合は常に、カウンター値が1ずつ増加します。参照が無効な場合は、カウンター値が1つ減少します。カウンターが0のオブジェクトはいずれも時間を再び使用することは不可能です。

利点:簡単な実装と高い判断効率。

短所:オブジェクト間の循環参照の問題を解決することは困難です。

JVMはこの判断方法を使用しませんが、FlashPlayerやPythonなどの言語で広く使用されています。

到達可能性分析

主流の商用プログラミング言語(Java、C#)の主流の実装では、到達可能性分析を使用して、オブジェクトが生きているかどうかを判断します。

原則:「GCルート」と呼ばれる一連の**参照**を開始点として、これらのノードから検索を開始し、検索が通過するパスを参照チェーン(参照チェーン)と呼びます。オブジェクトがない場合任意の参照チェーンによってGCルートに接続されている(つまり、オブジェクトがGCルートから到達できない)場合、オブジェクトが使用できないことを証明します。

注:本書ではGCルートをオブジェクトと呼んでいますが、知乎でRの答えを読んだ後は、参考文献の方が適していると思います。参照はスタックに格納された変数であり、オブジェクトはヒープに格納されたデータであり、通常、参照(つまり、参照変数)を介してオブジェクトを操作します。

ガベージコレクションが川であり、ヒープ内のオブジェクトが川のボートであると仮定すると、GCRootsが川のアンカーであると理解しましょう。アンカーポイントが川に流されることは絶対になく、アンカーポイントに接続されている船(つまり到達可能なオブジェクト)も流されることはありません。また、アンカーポイントから切り離されている船(つまり、到達不能なオブジェクト)は洗い流されません。)、川によってゆっくりと洗い流されます(つまり、ゴミによってリサイクルされます)。

画像の説明を追加してください
上図のように、object5、object6、object7は相互に関連していますが、GC Rootsに到達できないため、リサイクル可能なオブジェクトと判断されます。

どのオブジェクトをGCルートとして使用できますか?
Java言語では、GCルートとして使用できるオブジェクトは次のとおりです。

  1. 仮想マシンスタックで参照されるオブジェクト(スタックフレーム内のローカル変数テーブル)。
  2. メソッド領域のクラスの静的プロパティによって参照されるオブジェクト。
  3. メソッド領域の定数によって参照されるオブジェクト。
  4. ネイティブメソッドスタック内のJNI(つまり、一般にネイティブメソッド)によって参照されるオブジェクト。

ガベージコレクションについて話すGCルート
は若いGCで古いgenを収集しないため、現時点では古いgenのすべてのオブジェクトが有効であると見なされるため、これらのオブジェクトからの若いgenへの参照は有効であると見なす必要があります。したがって、これらの参照はルートセットの一部と見なされるため、フルGCよりも若いGCの方がGCルートが多くなります。

生きるか死ぬか

ただし、到達可能性分析アルゴリズムの到達不能オブジェクトでさえ「死ななければならない」わけではなく、現時点では一時的に「試用」段階にあります。オブジェクトが死んだと真に宣言するには、少なくとも2つのマーキングプロセスが必要です。

画像の説明を追加してください

最初のマーク

到達可能性分析の結果、オブジェクトにGC Rootsによって接続された参照チェーンがないことが判明した場合、オブジェクトは初めてマークされ、1回スクリーニングされます。スクリーニング条件は、オブジェクトがfinalize()メソッドを実行する必要があるかどうかです。オブジェクトがfinalize()メソッドをオーバーライドしない場合またはfinalize()メソッドが仮想マシンによって呼び出された場合、仮想マシンは両方のケースを「実行する必要がない」ものとして扱います。

2番目のマーク

オブジェクトがfinalize()メソッドを実行するために必要であると判断された場合、オブジェクトはF-Queueと呼ばれるキューに配置され、後で仮想マシンによって自動的に作成された優先度の低いFinalizerスレッドによって配置されます。実行します。ここでのいわゆる「実行」とは、仮想マシンがメソッドをトリガーするが、最後まで実行されるのを待つことを約束しないことを意味します。理由は、オブジェクトがfinalize()メソッドで遅い場合、または無限である場合です。ループが発生すると(より極端な場合)、F-Queueキュー内の他のオブジェクトが永久に待機し、メモリ再利用システム全体がクラッシュする可能性があります。

finalize()メソッドは、オブジェクトが死の運命から逃れるための最後のチャンスです。後で、GCはF-Queue内のオブジェクトに2番目の小さなスケールのマークを付けます。オブジェクトがfinalize()で正常に保存される場合-再接続するだけオブジェクトのクラス変数またはメンバー変数に自分自身を割り当てるなど、参照チェーン上の任意のオブジェクトとの関連付けを確立できます。これは、2番目のマークの「リサイクル対象」コレクションから削除されます。オブジェクトがまだエスケープがない場合、基本的には実際にリサイクルされます。

次のGCでは、エスケープされたオブジェクトは、もう一度マークを付けることによってのみ再利用されます。

finalize()メソッドのデメリット

finalize()メソッドは、すべての人が使用することをお勧めしません。この本では、「実行に費用がかかり、不確実であり、各オブジェクトの呼び出し順序を保証できない」と説明しています。
実際、より直感的な要因は、次のように、不適切に使用するとOOM(メモリオーバーフロー)が発生しやすいことです。

/**
 * @author 小关同学
 * @create 2021/10/4
 */
public class FinalizeTest {
    
    

    public static class Test{
    
    
        private byte[] content = new byte[1024*1024];

        protected void finalize(){
    
    
            System.out.println("finalize方法被执行");
        }
    }

    public static void main(String[] args) throws Exception{
    
    
        for (int i = 0;i<1000;i++){
    
    
            Test test = new Test();
        }
    }
}
# 虚拟机参数设置:
# 最大堆内存
-Xmx5m
# 最小堆内存
-Xms5m

実行結果:
ここに画像の説明を挿入
上図に示すように、メモリオーバーフローが発生しました。理由も前述しました。2番目のマーキングプロセスは、優先度の低いファイナライザスレッドによって実行されます。メインスレッドがスペースを占有する新しいテストオブジェクトを必死に作成している場合、ファイナライザスレッドも実行速度が遅いため、メモリスペースのクリア速度がメモリスペースの占有速度に追いつかず、メモリオーバーフローが発生します。

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

マークスイープ

「マークスイープ」(マークスイープ)アルゴリズムは最も基本的な収集アルゴリズムであり、それに基づいて後続の収集アルゴリズムが改善されます。リサイクルが必要なオブジェクトの場合、マークが完了した後、すべてのマークされたオブジェクトが均一にリサイクルされます(つまり、前の2つのマーキング)。

画像の説明を追加してください
上の写真はhttps://www.jianshu.com/p/74727c856da4からのものです

欠点:

  1. 効率の問題、マーキングとクリアの両方のプロセスはあまり効率的ではありません。
  2. スペースの問題、マークがクリアされた後、多数の不連続メモリフラグメントが生成されます。スペースフラグメントが多すぎると、将来、より大きなオブジェクトを割り当てる必要がある場合に、十分な連続メモリが見つからず、別のガベージコレクションアクションが発生します。事前にトリガーされます。

コピーアルゴリズム(コピー)

「コピー」アルゴリズムは、使用可能なメモリを容量によって2つの同じサイズのブロックに分割し、一度に1つだけを使用します。このメモリが使い果たされると、残っているオブジェクトが別のオブジェクトにコピーされ、使用されたメモリスペースが一度にクリーンアップされます。これにより、メモリの断片化の問題が解決されます。オブジェクトの生存率が高いほど、効率は低くなります

今日の商用仮想マシンは、レプリケーションアルゴリズムを使用して新世代を再生します。メモリを、より大きなEdenスペースと2つの小さなSurvivorスペースに分割し、毎回Edenと1つのSurvivorスペースを使用します。リサイクルするときは、エデンとサバイバーの生き残ったオブジェクトを一度に別のサバイバースペースにコピーし、最後に使用したばかりのエデンとサバイバースペースをクリーンアップします。

HotSpot JVMは、若い世代を3つの部分に分割します。1つのEdenエリアと2つのSurvivorエリア(それぞれFromとToと呼ばれます)、デフォルトの比率は8:1:1、EdenとFrom Survivorのデフォルトのサイズ比率は8:1です。つまり、各新世代で使用可能なメモリスペースは、新世代の容量全体の90%(80%+ 10%)であり、メモリの10%(サバイバーエリアへ)のみが「浪費」されます。

注:サバイバースペースが十分でない場合は、割り当ての保証(ハンドルプロモーション)のために他のメモリ(ここでは旧世代を指します)に依存する必要があります。
画像の説明を追加してください

収集プロセスの説明:

  1. GCの開始時には、オブジェクトはEdenエリアにのみ存在し、From Survivorエリア、ToSurvivorエリアは空です。

  2. GCの直後に、Edenエリアに残っているすべてのオブジェクトがToSurvivorエリアにコピーされます。

  3. From Survivorエリアでは、生き残ったオブジェクトが年齢の値に応じてどこに行くかを決定します(From SurvivorエリアでオブジェクトがマイナーGCを生き残るたびに、年齢は1年ずつ増加します)。年齢が特定の値(-XX:MaxTenuringThresholdによって)に達したオブジェクトは古い年齢に移動され、しきい値に達していないオブジェクトは[生存者へ]領域にコピーされます。

  4. このGCの後、エデンエリアとフロムサバイバーエリアはクリアされました。このとき、FromSurvivorとToSurvivorはそれぞれの役割を交換します。つまり、新しいToSurvivorは最後のGCの前のFromSurvivorであり、新しいFromSurvivorは最後のGCの前のToSurvivorです。いずれの場合も、To Survivorという名前のエリアは空であることが保証されます。つまり、空のSurvivorSpaceが常に存在しますマイナーGCは、To Survivor領域がいっぱいになるまで(つまり、To Survivor領域にコピーされると、To Survivor領域を一度にロードできない)、To Survivor領域がいっぱいになるまで、このプロセスを繰り返します。古い世代に直接移動します。

マーク-コンパクト

「Mark-Compact」アルゴリズムは、老後をリサイクルするためのアルゴリズムです。そのマーキングプロセスは「mark-clean」アルゴリズムと同じですが、後続の手順では、リサイクル可能なオブジェクトを直接クリーンアップするのではなく、すべての生き残ったオブジェクトはクリーンアップされます。オブジェクトは一方の端に移動され、次に端の境界の外側のメモリがクリーンアップされます。

ここに画像の説明を挿入

上の写真は、https://blog.51cto.com/u_4837471/2324575からのものです。

ジェネレーションコレクション

「GenerationalCollection」アルゴリズムは、オブジェクトのライフサイクルに応じてメモリをいくつかのブロックに分割します。通常、Javaヒープは新世代と旧世代に分割されるため、個々の世代の特徴。

新世代では、ガベージコレクションごとに多数のオブジェクトが停止し、存続するのはごくわずかであるため、レプリケーションアルゴリズムが使用され、存続するオブジェクトをコピーするわずかなコストで収集を完了できます。
旧世代では、オブジェクトの生存率が高すぎるため、「マークスイープ」または「マークツークリーン」アルゴリズムを使用して、割り当てを保証する余分なスペースなしで再利用する必要があります。

アルゴリズムの実装原理

ルートノードを列挙する

ルートノードを列挙することは、GCルートを見つけることです。前述したように、HotSpot仮想マシンでは、到達可能性分析を使用して、オブジェクトをリサイクルする必要があるかどうかを判断します。アクセシビリティ分析では、「ソース」、つまりルートノード(GCルート)を見つける必要があります。

質問

  1. 多くの時間を消費します。
    以前のアクセシビリティ分析から、GCルートは主にグローバル参照(定数または静的属性)と実行コンテキスト(スタックフレーム内のローカル変数テーブル)にあることがわかります。
    これらの大量のデータで参照を1つずつチェックします。多くの時間を消費します。
  2. GCストール。
    到達可能性分析中は、実行システム全体の一貫性を確保する必要があり、オブジェクトの参照関係を変更することはできません。
    すべてのJava実行スレッドをGC中に一時停止する必要があります(「StopThe World」、STWと呼ばれます)。
    (一時停止はほとんどありません)。 CMSコレクターでは、ルートノードの列挙も一時停止する必要があります)
    STWは、バックグラウンドでJVMによって自動的に開始および完了されます。ユーザーが非表示になると、ユーザーの通常の作業スレッドはすべて停止します。

概要:到達可能性分析に費やす時間を短縮するには、SWTの時間を可能な限り短縮し、それによって仮想マシンのダウンタイムの時間を短縮します。

解決

到達可能性分析にかかる時間を短縮するために、Hotspot仮想マシンはOopMapと呼ばれるデータ構造のセットを使用します。これにより、仮想マシンは、毎回参照チェーン全体を直接トラバースすることなく、オブジェクト参照が格納されている場所を直接知ることができます。システムが静止した後、完全なローカリティと実行コンテキストですべての参照場所を1つずつチェックする必要はありません。

ホットスポットでは、オブジェクトのタイプ情報には独自のOopMapがあり、オブジェクトのタイプのどのオフセットにどのタイプのデータがあるかを記録します。したがって、オブジェクトの先頭から外側へのスキャンは正確であり、これらのデータはクラスのロードプロセス中に計算されます。

OopMapの役割:

  1. フルスタックスキャンを回避し、ルートノードの列挙を高速化します
  2. ホットスポットが正確なGCを達成するのを助ける

保守的なGC

保守的なGCは、ポインター(参照)と非ポインターを認識しないGCです。

欠点:

  1. 参照と非参照を識別しないと、再利用されるべきオブジェクトがいくつか発生しますが、それらを指している疑いのあるポインターがあり、GCコレクションからエスケープされます。
  2. 疑わしいポインターが実際のポインターであるかどうかがわからないため、それらの値を書き換えることはできません。オブジェクトの移動はポインターの変更を意味するため、次のようなオブジェクトを移動しないGCアルゴリズムのみを使用できます。Mark-Sweep

半保存的GC

半保存的GCは、スタックに参照情報を記録しませんが、オブジェクトに参照情報を記録します

欠点:

  1. 実行時に、オブジェクトに十分なメタデータが必要です

正確なGC

仮想マシンは、メモリ内のすべての場所にあるデータの種類と、GCヒープ内の参照を指しているかどうかを知る必要があります。

いくつかの正確なGC実装方法:

  1. データ自体にタグを付けます(半保存的GCでは一般的です)。
  2. コンパイラにメソッドごとに特別なスキャンコードを生成させます。
  3. 外部からのタイプ情報を記録し、次のようなマッピングテーブルとして保存します。ホットスポットのOopMap。

セーフポイント

OopMapを使用すると、HotSpotはGCルートの列挙を迅速かつ正確に完了することができます。しかし、別の疑問が生じます。OopMapをどこで作成するのでしょうか。
プログラムの実行中、参照の変更は常に発生します。各命令がOopMapを生成すると、スペースを取りすぎるため、セーフポイント(セーフポイント)があります。
GCの一時停止の前に参照の変更の記録が完了している限り、セーフポイントでのみGCの一時停止を実行します。

安全な場所の選び方

セーフティポイントの選択は、基本的にプログラムが長時間実行できる特性があるかどうかに基づいており、ターン、異常ジャンプなどの機能を備えた命令でセーフポイントが生成されます。

セーフポイントがスレッドを中断する方法

選択できるオプションは2つあります。

  1. プリエンプティブサスペンション
    GCが発生すると、最初にすべてのスレッドが中断されます。スレッドが中断された場所が安全なポイントにないことが判明した場合、スレッドは復元され、安全なポイントまで「実行」されます。
  2. Active Interrupt(Voluntary Suspension)
    GCがスレッドに割り込みをかける必要がある場合、GCはスレッドを直接操作せず、単にフラグを設定します。各スレッドが実行されると、アクティブにフラグをポーリングし、割り込みフラグがtrue、それはそれ自体を中断して一時停止します。ポーリングフラグがセーフポイントと一致する場所に加えて、オブジェクトの作成でメモリを割り当てる必要がある場所。

HotSpotはプロアクティブな割り込みを使用します。

安全な地域

セーフティポイントは、プログラムの実行時に、短時間でGCに入ることができるセーフティポイントに遭遇することを保証しますが、スレッドがCPU時間を割り当てない場合、スレッドはスリープまたはブロック状態である必要があります。 JVMの割り込み要求は、サスペンドするための安全なポイントに移動します。SafeRegionはこの問題を解決します。

セーフエリアとは、コードの一部で参照関係が変更されないことを意味します。この領域のどこからでもGCを開始しても安全です。

コードがセーフエリアで実行されると、最初にセーフエリアに入ったことを識別します。したがって、この期間中にJVMがGCを開始した場合、セーフエリアで自分自身を識別するスレッドを気にする必要はありません。セーフエリアを離れると、システムがルートノードの列挙(またはGCプロセス全体)を完了したかどうかを確認します。完了した場合、スレッドは実行を継続します。完了していない場合は、安全に離れることができるというシグナルを受信するまで待機する必要があります。安全地帯。

PS:私の個人ブログにアクセスして、より多くのコンテンツを表示することもでき
ます。個人ブログのアドレス:Xiaoguanクラスメートのブログ

おすすめ

転載: blog.csdn.net/weixin_45784666/article/details/120601240