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 () メソッドはシステムによって自動的に一度だけ呼び出されます。オブジェクトが次回回収される際、その 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 が老年代に昇進する平均サイズが老年代の残り空間を超える場合があります。