JVM シリーズ - JVM のガベージコレクション#
内容来自:
一、ガベージコレクションの判定#
参照の種類#
-
強い参照(Strong Reference)
: 強い参照が存在する限り、ガベージコレクタは参照されているオブジェクトを決して回収しない。 -
ソフト参照(Soft Reference)
: JVM がメモリ不足と判断した場合、OutOfMemoryError を投げる前にソフト参照が指すオブジェクトを回収する。 -
弱い参照(Weak Reference)
: JVM がガベージコレクションを行う際、メモリが十分であっても、必ず回収されるソフト参照に関連付けられたオブジェクト。 -
仮想参照(Phantom Reference)
: 幽霊参照または幻影参照とも呼ばれ、最も弱い参照関係である。オブジェクトに仮想参照が存在するかどうかは、その生存時間に影響を与えない。これは、オブジェクトが finalize された後に何かを行うことを保証するメカニズムを提供するだけで、通常はいわゆるポストモーテムクリーニングメカニズムに使用される。
オブジェクトが生存しているかの判定#
もしオブジェクトが他のオブジェクトや変数から参照されていない場合、それは無効なオブジェクトであり、回収される必要がある。
参照カウント法#
オブジェクトヘッダーにはカウンターが維持され、オブジェクトが参照されるたびにカウンターが + 1 される。参照が無効になるとカウンターは - 1 される。カウンターが 0 になると、そのオブジェクトは無効と見なされる。
参照カウントアルゴリズムの実装は簡単で、判定効率も高いが、主流の Java 仮想マシンでは参照カウントアルゴリズムをメモリ管理に選択していない。主な理由は、オブジェクト間の循環参照
の問題を解決するのが難しいからである:
例えば、オブジェクト objA と objB がそれぞれフィールド instance を持ち、objA.instance = objB および objB.instance = objA とすると、互いに参照し合っているため、参照カウントはどちらも 0 にならず、参照カウントアルゴリズムは GC に回収を通知できない。
到達性分析法#
GC Roots に直接または間接的に関連するすべてのオブジェクトは有効なオブジェクトであり、GC Roots に関連しないオブジェクトは無効なオブジェクトである。
GC Rootsとは:
- Java 仮想マシンスタック(スタックフレーム内のローカル変数テーブル)で参照されるオブジェクト
- ローカルメソッドスタックで参照されるオブジェクト
- メソッドエリアで定数が参照するオブジェクト
- メソッドエリアでクラスの静的属性が参照するオブジェクト
GC Roots にはヒープ内のオブジェクトが参照するオブジェクトは含まれないため、循環参照の問題は発生しない。
ヒープ内の無効オブジェクトの回収#
到達性分析で到達不可能なオブジェクトも、生存の可能性がないわけではない。
finalize () の実行が必要かの判定#
JVM はこのオブジェクトが finalize () メソッドを実行する必要があるかどうかを判断する。オブジェクトが finalize () メソッドをオーバーライドしていない場合、または finalize () メソッドがすでに仮想マシンによって呼び出されている場合、それは **「実行する必要がない」** と見なされる。この場合、オブジェクトはほぼ確実に回収される。
オブジェクトが finalize () メソッドを実行する必要があると判断された場合、そのオブジェクトは F-Queue キューに入れられ、仮想マシンはこれらの finalize () メソッドを低い優先度で実行するが、すべての finalize () メソッドが終了することは保証されない。もし finalize () メソッドに時間のかかる操作があれば、仮想マシンはそのメソッドへのポインタを直接停止し、オブジェクトをクリアする。
オブジェクトの再生または死亡#
finalize () メソッドを実行中に this をある参照に代入した場合、そのオブジェクトは再生される。そうでなければ、ガベージコレクタによってクリアされる。
注意:
任意のオブジェクトの finalize () メソッドはシステムによって自動的に 1 回だけ呼び出される。オブジェクトが次回の回収に直面する場合、その finalize () メソッドは再度実行されることはなく、finalize () 内で自救を続けることは無効になる。
メソッドエリアのメモリ回収#
メソッドエリアにはライフサイクルが長いクラス情報、定数、静的変数が格納され、毎回のガベージコレクションでは少量のガベージがクリアされる。メソッドエリアでは主に 2 種類のガベージがクリアされる:
- 廃棄された定数
- 無用なクラス
廃棄された定数の判定#
定数プール内の定数が他の変数やオブジェクトから参照されていない限り、これらの定数はクリアされる。例えば、文字列 "bingo" が定数プールに入ったが、現在のシステムにはこの定数プール内の "bingo" 定数を参照する String オブジェクトがなく、他の場所でもこのリテラルを参照していない場合、必要に応じて "bingo" 定数は定数プールからクリアされる。
無用なクラスの判定#
クラスが「無用なクラス」であるかどうかを判定する条件は厳しい。
- そのクラスのすべてのオブジェクトがすでにクリアされている
- そのクラスをロードした ClassLoader が回収されている
- そのクラスの java.lang.Class オブジェクトがどこでも参照されておらず、どこでもリフレクションを通じてそのクラスのメソッドにアクセスできない。
クラスが仮想マシンによってメソッドエリアにロードされると、ヒープ内にそのクラスを表すオブジェクトが作成される。このオブジェクトはクラスがメソッドエリアにロードされるときに作成され、メソッドエリアでそのクラスが削除されるときにクリアされる。
メモリリーク#
Java では長いライフサイクルのオブジェクトが短いライフサイクルの参照を保持している
場合、メモリリークが発生する可能性が高い。例えば、シングルトンパターンでは、このシングルトンオブジェクトのライフサイクルをプログラム全体のライフサイクルとほぼ同じと見なすことができるため、長いライフサイクルのオブジェクトとなる。このオブジェクトが他のオブジェクトの参照を保持している場合、メモリリークが発生する可能性がある。
詳細な内容については、JAVA メモリリークの詳細(原因、例、解決策)を参照してください。
二、ガベージコレクションアルゴリズム#
無効なオブジェクト、無用なクラス、廃棄された定数を判定した後、残りの作業はこれらのガベージを回収することです。一般的なガベージコレクションアルゴリズムには以下のものがあります:
マーク - クリアアルゴリズム#
どのデータをクリアする必要があるかを判断し、それらにマークを付けてから、マークされたデータをクリアします。
この方法には 2 つの欠点があります:
- 効率の問題:マークとクリアの 2 つのプロセスの効率は高くありません。
- 空間の問題:マーククリアの後、大量の不連続なメモリの断片が生成され、断片が多すぎると、将来大きなオブジェクトを割り当てる必要があるときに、十分な連続メモリを見つけられず、再度ガベージコレクションをトリガーせざるを得なくなります。
コピーアルゴリズム(新生代)#
効率の問題を解決するために、「コピー」収集アルゴリズムが登場しました。これは、使用可能なメモリを容量に応じて同じサイズの 2 つのブロックに分割し、毎回そのうちの 1 つだけを使用します。このブロックのメモリが使い果たされ、ガベージコレクションを行う必要があるとき、生存しているオブジェクトを別のブロックにコピーし、最初のブロックのメモリをすべてクリアします。このアルゴリズムには利点と欠点があります:
- 利点:メモリの断片化の問題が発生しません。
- 欠点:メモリが元の半分に縮小され、空間が無駄になります。
空間利用率の問題を解決するために、メモリを 3 つのブロックに分けることができます:Eden、From Survivor、To Survivor、比率は 8:1:1 で、毎回 Eden とそのうちの 1 つの Survivor を使用します。回収時には、Eden と Survivor の中で生存しているオブジェクトを一度に別の Survivor 空間にコピーし、最後に Eden と先ほど使用した Survivor 空間をクリアします。これにより、無駄になるメモリは 10% だけになります。
しかし、毎回の回収で生存するオブジェクトが 10% を超えることは保証できず、Survivor 空間が不足する場合、他のメモリ(つまり老年代)に依存して割り当ての保証を行う必要があります。
割り当て保証#
老年代の廃棄データをクリアすることで、老年代の空きスペースを拡大し、新生代の保証を行います。
JDK 6 Update 24 以前:
Minor GC が発生する前に、仮想マシンは老年代の最大利用可能な連続スペースが新生代のすべてのオブジェクトの合計スペースより大きいかどうかを最初に確認します。この条件が成立すれば、Minor GC は安全であることが保証されます。成立しない場合、仮想マシンは HandlePromotionFailure の値が保証失敗を許可するように設定されているかどうかを確認します。もしそうであれば、老年代の最大利用可能な連続スペースが過去に老年代に昇進したオブジェクトの平均サイズより大きいかどうかを確認し、大きければリスクがあっても Minor GC を試みます。小さければ、または HandlePromotionFailure がリスクを許可しないように設定されている場合、その時点で Full GC に切り替える必要があります。
JDK 6 Update 24 以降:
老年代の連続スペースが新生代オブジェクトの合計サイズまたは過去の昇進の平均サイズより大きい限り、Minor GC が行われます。そうでなければ Full GC が行われます。
このプロセスが割り当て保証です。
マーク - 整理アルゴリズム(老年代)#
ガベージを回収する前に、まず廃棄オブジェクトにマークを付け、その後未マークのオブジェクトを一方に移動し、最後にもう一方の領域をクリアします。
これは老年代のガベージコレクションアルゴリズムです。老年代のオブジェクトは一般的に寿命が長いため、毎回のガベージコレクションでは大量のオブジェクトが生存しており、コピーアルゴリズムを使用すると毎回大量の生存オブジェクトをコピーする必要があり、効率が非常に低くなります。
世代別収集アルゴリズム#
オブジェクトの生存周期の違いに基づいて、メモリをいくつかのブロックに分割します。一般的には Java ヒープを新生代と老年代に分け、それぞれの年代の特徴に最も適した収集アルゴリズムを採用します。
- 新生代:コピーアルゴリズム
- 老年代:マーク - クリアアルゴリズム、マーク - 整理アルゴリズム
三、ガベージコレクタ#
新生代ガベージコレクタ#
Serial ガベージコレクタ(単スレッド)#
1 つの GC スレッドを開いてガベージ回収を行い、ガベージコレクション中にすべてのユーザースレッドを停止します(Stop The World)。
一般的にクライアントアプリケーションは必要なメモリが少なく、あまり多くのオブジェクトを作成せず、ヒープメモリも大きくないため、ガベージコレクタの回収時間は短く、この間にすべてのユーザースレッドを停止しても明らかなカクつきを感じることはありません。したがって、Serial ガベージコレクタはクライアントに適しています。
Serial コレクタは 1 つの GC スレッドのみを使用するため、スレッド切り替えのオーバーヘッドを回避し、シンプルで効率的です。
ParNew ガベージコレクタ(マルチスレッド)#
ParNew は Serial のマルチスレッド版です。複数の GC スレッドが並行してガベージクリーニングを行いますが、クリーニングプロセスは依然として Stop The World が必要です。
ParNew は **「低停止時間」** を追求し、Serial との唯一の違いは、ガベージコレクションにマルチスレッドを使用していることです。マルチ CPU 環境では、性能が Serial よりも一定程度向上しますが、スレッド切り替えには追加のオーバーヘッドが必要なため、単一 CPU 環境では Serial よりもパフォーマンスが劣ります。
Parallel Scavenge ガベージコレクタ(マルチスレッド)#
Parallel Scavenge は ParNew と同様に、マルチスレッドの新生代ガベージコレクタです。しかし、両者には大きな違いがあります:
- Parallel Scavenge:CPU スループットを追求し、短時間で指定されたタスクを完了できるため、インタラクティブでないバックグラウンド計算に適しています。
- ParNew:ユーザーの停止時間を低減することを追求し、インタラクティブなアプリケーションに適しています。
高いスループットを追求するためには、GC が実際の作業を行う時間を減らす必要がありますが、GC が偶然に実行されることは、GC が実行されるたびに多くの作業があることを意味します。なぜなら、この期間中にヒープ内に蓄積されたオブジェクトの数が非常に多いためです。単一の GC は完了するのにより多くの時間を要し、その結果、より高い停止時間が発生します。低停止時間を考慮すると、GC を頻繁に実行してより迅速に完了させるのが最善ですが、これが逆にスループットの低下を引き起こします。
- パラメータ
-XX:GCTimeRadio
ガベージ回収時間が総 CPU 時間の何パーセントを占めるかを設定します。 - パラメータ
-XX:MaxGCPauseMillis
ガベージ処理プロセスの最大停止時間を設定します。 - コマンド
-XX:+UseAdaptiveSizePolicy
適応戦略を有効にします。ヒープのサイズと MaxGCPauseMillis または GCTimeRadio を設定するだけで、コレクタは新生代のサイズ、Eden と Survivor の比率、オブジェクトが老年代に入る年齢を自動的に調整し、設定した MaxGCPauseMillis または GCTimeRadio に最大限近づけます。
老年代ガベージコレクタ#
Serial Old ガベージコレクタ(単スレッド)#
Serial Old コレクタは Serial の老年代版で、単スレッドコレクタであり、1 つの GC スレッドのみを有効にし、クライアントアプリケーションに適しています。彼らの唯一の違いは、Serial Old が老年代で動作し、「マーク-整理」
アルゴリズムを使用することです。Serial は新生代で動作し、「コピー」
アルゴリズムを使用します。
Parallel Old ガベージコレクタ(マルチスレッド)#
Parallel Old コレクタは Parallel Scavenge の老年代版で、CPU スループットを追求します。
CMS ガベージコレクタ#
CMS(Concurrent Mark Sweep、並行マークスイープ)コレクタは、** 最短回収停止時間を目指すコレクタ(低停止を追求)** であり、ガベージコレクション中にユーザースレッドと GC スレッドが並行して実行されるため、ガベージコレクション中にユーザーは明らかなカクつきを感じません。
CMS コレクタは「マーク-クリア」
アルゴリズムを実装しており、全体のプロセスは 4 つのステップに分かれています:
初期マーク
:Stop The World、1 つの初期マークスレッドを使用して、GC Roots に直接関連するすべてのオブジェクトにマークを付けます。並行マーク
:複数のマークスレッドを使用し、ユーザースレッドと並行して実行します。このプロセスでは到達性分析を行い、すべての廃棄オブジェクトにマークを付けます。速度は非常に遅いです。再マーク
:Stop The World、複数のマークスレッドを使用して並行して実行し、先ほどの並行マークプロセスで新たに出現した廃棄オブジェクトにマークを付けます。並行クリア
:1 つの GC スレッドを使用し、ユーザースレッドと並行して実行し、先ほどマークされたオブジェクトをクリアします。このプロセスは非常に時間がかかります。
並行マークと並行クリアのプロセスは最も時間がかかり、ユーザースレッドと一緒に作業できるため、全体的に CMS コレクタのメモリ回収プロセスはユーザースレッドと並行して実行されます。
欠点
- CPU リソースに敏感で、スループットが低い;
- 浮動ゴミを処理できず、頻繁に Full GC を引き起こす;
- 使用される回収アルゴリズム -「マーク - クリア」アルゴリズムは、収集終了時に大量の空間の断片を生成します。
G1 汎用ガベージコレクタ#
G1(Garbage-First)は、サーバーアプリケーション向けのガベージコレクタで、新生代と老年代の概念がなく、ヒープを独立した Region に分割します。G1 コレクタはバックグラウンドで優先リストを維持し、ガベージ回収を行う際に、まず各 Region 内のガベージの量を推定し、許可された回収時間に基づいて、最も価値のある Region を優先的に選択して回収します。この Region によるメモリ空間の分割と優先度のある領域回収方式により、G1 コレクタは限られた時間内に可能な限り高い収集効率を保証します(メモリを分割してゼロにする)。
全体的に見て、G1 は「マーク - 整理」アルゴリズムを基にしたコレクタであり、局所的(2 つの Region 間)には「コピー」アルゴリズムを基にしているため、実行中にメモリ空間の断片化が発生しません。
各 Region には Remembered Set があり、その領域内のすべてのオブジェクトが参照するオブジェクトが所在する領域を記録します。到達性分析を行う際には、GC Roots に Remembered Set を追加するだけで、ヒープ全体をスキャンする必要がなくなります。したがって、オブジェクトとその内部で参照されるオブジェクトが同じ Region に存在しない場合でも、ガベージ回収時にヒープ全体をスキャンして完全な到達性分析を行う必要はありません。
Remembered Set の操作を考慮しない場合、G1 コレクタの作業プロセスは以下のステップに分かれます:
初期マーク
:Stop The World、1 つの初期マークスレッドを使用して、GC Roots に直接関連するすべてのオブジェクトにマークを付けます。並行マーク
:1 つのマークスレッドを使用し、ユーザースレッドと並行して実行します。このプロセスでは到達性分析を行い、速度は非常に遅いです。最終マーク
:Stop The World、複数のマークスレッドを使用して並行して実行します。フィルタリング回収
:廃棄オブジェクトを回収します。この時も Stop The World が必要で、複数のフィルタリング回収スレッドを使用して並行して実行します。
補足拡張#
JVM が Full GC をトリガーする状況#
System.gc () メソッドの呼び出し#
このメソッドの呼び出しは、JVM に Full GC を行うように提案しますが、これは提案に過ぎず、必ずしもそうなるわけではありません
。しかし、多くの状況で Full GC をトリガーし、Full GC の頻度を増加させることがあります。通常、仮想マシンにメモリを管理させるだけで十分であり、-XX:+DisableExplicitGC
を使用して System.gc () の呼び出しを禁止することができます。
老年代の空間不足#
老年代の空間不足は Full GC 操作をトリガーします。この操作後も空間が不足している場合、次のエラーが発生します:java.lang.OutOfMemoryError: Java heap space
永続世代の空間不足#
JVM 仕様の実行時データ領域内のメソッドエリアは、HotSpot 仮想マシンでは永続世代(Permanent Generation)とも呼ばれ、クラス情報、定数、静的変数などのデータを格納します。システムがロードするクラス、リフレクションするクラス、および呼び出すメソッドが多い場合、永続世代が満杯になる可能性があり、Full GC をトリガーします。Full GC 後も回収できない場合、JVM は次のエラーメッセージをスローします:java.lang.OutOfMemoryError: PermGen space
その他#
-
CMS GC 中に promotion failed と concurrent mode failure が発生する。
promotion failed は、前述の割り当て保証失敗を指し、concurrent mode failure は CMS GC を実行中にオブジェクトを老年代に配置しようとしたが、老年代の空間が不足しているために発生します。 -
統計的に得られた Minor GC が老年代に昇進する平均サイズが老年代の残り空間より大きい。