JVM ガベージコレクション
ガベージコレクション(Garbage Collection)とは
ガベージコレクション(GC, Garbage Collection)は、Java プロセスでこれ以上使用しないメモリを自動的に解放する JVM の処理である。
Java Runtime 時に Heap 領域へ保存されるオブジェクトは、整理しなければ蓄積し続け、OutOfMemoryError(OOME)が発生する可能性がある。これを防ぐため、JVM では定期的に使用されていないオブジェクトを収集して整理する GC を行う。
C 言語の場合は、malloc()、free() などを使ってメモリを割り当て、手動でメモリを解放しなければならない。この不便さは、Java では GC 技術を使って自動的にメモリを解放することで解消され、開発者をメモリ管理から解放した。
Java はプログラムコードでメモリを明示的に指定して解放しない。オブジェクトを null に指定したり、System.gc() メソッドを呼び出したりする開発者もいる。null に指定することは大きな問題ではないが、System.gc() メソッドの呼び出しはシステム性能に非常に大きな影響を与えるため、System.gc() メソッドは絶対に使用してはならない。
JVM の GC について理解するには、まず JVM メモリ構造を理解する必要がある。JVM メモリ構造については、こちらを参考にしてほしい。
不要なメモリ領域
メモリの使用状況を意識せずにアプリケーションを作ると、使われなくなったごみデータ(ガベージ)が発生する。
たとえば、次のようなクラスがあると仮定する。
class TreeNode {
public TreeNode left, right;
public int data;
TreeNode(TreeNode l, TreeNode r, int d) {
left = l; right = r; data = d;
}
public void setLeft(TreeNode l) { left = l;}
public void setRight(TreeNode r) {right = r;}
}
次の処理によって TreeNode を作成する。
TreeNode left = new TreeNode(null, null, 13);
TreeNode right = new TreeNode(null, null, 19);
TreeNode root = new TreeNode(left, right, 17);
このようにすると、ルートノードが left ノードと right ノードを参照する。

ここで right ノードを変更する処理を追加したと仮定する。
root.setRight(new TreeNode(null, null, 21));
すると、もともと right ノードに入っていた 19 番ノードは誰からも参照されなくなり、下図のような状態になる。

この状態では、data=19 の TreeNode インスタンスはどこからも参照されないオブジェクト、つまり到達不能状態のオブジェクト(Unreachable object)となるため、ガベージになる。
これ以上使用されないデータが発生し続けると、不要なメモリが蓄積し、やがて容量の限界に達する。これを未然に防ぐため、ヒープ領域の不要なメモリを自動的に解放する仕組みとして GC(ガベージコレクション)が必要になった。
GC およびヒープ領域の役割
前述のとおり、GC はこれ以上必要のないメモリを解放するメカニズムである。
メモリ内のデータを調べ、参照があれば有効なデータとして残り、参照がなければ不要と判断して解放する。ただし、単純にすべてのメモリ空間を調べると効率が悪いため、データの存在期間に応じて内部的に分けて管理される。
新しいデータは Young Generation、古いデータは Old Generation、事前に変更されにくいと判断されるデータは Permanent Generation と呼ばれる。

基本的にメモリへの割り当ては頻繁に発生するが、そのほとんどは参照されなくなると仮定し、短命なデータ(Young Generation)と長期間参照されるデータ(Old Generation)に分けている。 これにより、Young Generation に含まれるデータだけを GC 対象として効率的に確認できる。
また Permanent Generation という領域も存在し、ここにはロードされたクラスの情報など、ある程度変化しないことが保証されるデータが保存される。
Permanent Generation メモリ領域は Java 8 が登場してなくなり、Metaspace 領域が作られた。Metaspace 領域についての詳しい内容はこちらを参考にしてほしい。
参考: OpenJDK ドキュメント
GC サイクル
アプリケーションで使用するヒープ領域は、GC 実行領域別に大きく Young(Eden, Survivor1, Survivor2)領域と Old(Tenured)領域に分けられる。
ヒープ領域の Young 領域は下図のように Eden と Survivor に分かれており、それぞれの領域を使って GC が行われる。

各領域の役割は次のとおりである。
- Eden
- Java オブジェクトが生成されるとすぐに割り当てられるメモリ領域である。
- 定期的なガベージコレクションによって生き残ったオブジェクトは Survivor へ移動する。
- Survivor1, Survivor2
- GC 後に解放されず、Old には行かないデータ(便宜上 2 つあり、1 と 2 を付けているだけである)
- Tenured
- 指定された回数の GC を経て生き残ったデータが Old へ移動される。
GC が実行される領域に応じて、Minor GC と Major GC(Full GC)に分けられる。
- Young 領域の GC を Minor GC という。
- Old 領域の GC は Full GC(または Major GC)という。
Full GC が発生すると、瞬間的に Java アプリケーションの動作が中断される stop-the-world が発生するため、比較的遅く、性能と安定性に大きな影響を与える可能性がある。
Minor GC
Young Generation のみを対象にした GC を Minor GC という。次の特徴がある。
- 処理時間が短い。
- Eden がいっぱいになると発生する。
- 一定回数 GC 対象になると Old へ移動する。
- GC 中はプロセス処理が停止する(Stop the world)。
文章より図で説明するほうが分かりやすいため、図で説明する。
新しくメモリが割り当てられて Eden 領域がいっぱいになると、Minor GC が発生する。
参照がないデータは削除されるが、有効なデータは Survivor 領域にコピーされる。そして Eden 領域はすべて空になる。

その後、この状態で再び Eden 領域がいっぱいになると、再び Minor GC が発生して下図のようになる。

今回は GC 後にすべて Survivor2 領域に入った。Survivor 領域は、どちらか空いている側にデータがコピーされ、1 と 2 を行き来する。そのため、常に Survivor1 と Survivor2 のどちらか一方は空の状態に保たれる。
また、Eden 領域と同様に、Survivor 領域で参照されないデータは削除される。
次は Old 領域への昇格である。GC が発生するたびに Young 領域のデータにはその回数が記録され、一定回数を超えると Old へ移動する。

このように複数回 GC を繰り返すと、Young 領域から Old 領域への移動が発生する。この回数はオプションで指定でき、Old 領域へ行く頻度を次のオプションで制御できる。
-XX:MaxTenuringThreshold=N
Full GC
Young 領域から Old 領域へデータが移る仕組みについて見てきた。これだけであれば Old 領域の容量は常に増え続け、いつか容量の限界に達する。そのため、このとき Full GC が発生する。Old 領域への割り当てに失敗した時点で Full GC が発生し、Old 領域と Young 領域の両方を含めてメモリを清掃する。

これにより、Old 領域でこれ以上必要のない空間を確保し、Survivor 領域にあったデータをコピーできるようになる。
Minor GC と同様に、Full GC 中にもアプリケーションは停止する。そして Old 領域に入っている分だけ停止時間も長くなるため、メモリはできるだけ Young 領域で解放されるようにし、Full GC の発生を最小化することが重要である。
GC サイクルのまとめ
GC サイクルをまとめると次のとおりである。
- Eden 領域がいっぱいになると Minor GC が発生する。
- Minor GC によって Young 領域を解放し、条件を満たすと Old へ昇格される。
- Old 領域がいっぱいになると Full GC が発生する。
- Full GC で Old 領域を解放し、昇格できる空間を確保する。
Automatic Garbage Collection
ガベージコレクションの過程について見てみよう。
Automatic Garbage Collection は、ヒープメモリ内でどのオブジェクトが使用中で、どのオブジェクトがそうでないかを判定し、使用されていないオブジェクトを削除する過程である。使用中または参照されているオブジェクトとは、プログラムのどこかがまだそのオブジェクトへのポインタを保持しているという意味である。
C 言語のようなプログラミング言語ではメモリを手動で割り当てたり解放したりしなければならないが、Java では Garbage Collector によって自動的にメモリが解放される。Automatic GC の基本的な過程を見てみよう。
Step 1: Marking
Marking はメモリを断片単位で識別する過程である。
ガベージコレクタは、メモリで参照されているオブジェクト(reachable/live object)を確認し、参照されていないオブジェクト(unreachable object)が何かをマーキングする手順を行う。

参照されるオブジェクトは青色で、残りはオレンジ色で示している。すべてのオブジェクトは、マーキング過程で判定するためにスキャンされる。この過程はシステム内のすべてのオブジェクトをスキャンする必要があるため、時間がかかる。
Step 2: Normal Deletion
Normal Deletion は、参照されていないオブジェクトを削除する過程である。
ガベージコレクタは、参照されていないオブジェクト(unreachable object)を削除する。

参照されていないオブジェクトを削除し、参照されるオブジェクトと free space へのポインタ位置を残す。Memory allocator は、新しく割り当てられるオブジェクトのために free space への参照を保持する。
Step 2a: Deletion with Compacting
Deletion の性能を向上させるため、参照されていないオブジェクトを削除するだけでなく、残った空間を圧縮する過程である。
ガベージコレクタの一部は、memory をより効果的に使用するため、参照されていないオブジェクト(unreachable object)を削除すると同時に圧縮を行うこともある。

オブジェクトを 1 か所に集めることで、新しいメモリ割り当てをより簡単かつ高速にできる。Memory Allocator は free space の開始アドレスだけを持っていればよい。その後、新しいオブジェクトを順次割り当てる。
出典: Oracle 公式ドキュメント: Java Garbage Collection Basics
GC アルゴリズム
GC アルゴリズムはいくつかあり、代表的な次の 4 つについて見ていく。
- Serial GC
- Parallel GC
- CMS (Concurrent Mark & Sweep) GC
- ガベージファースト GC(G1GC)
GC アルゴリズムは、スループット(throughput、処理量)と応答時間(Responsiveness: 敏感度)を考慮して分類される。
- アプリケーション停止型
- Serial GC, Parallel GC
- シングルコア、マルチコアの基本 GC
- 処理量を重視するが、GC で停止する時間が長くなるため、応答時間の要件を満たせないことがある。
- アプリケーションと並行して同時処理する型
- CMS, G1GC
- マルチコア環境で Parallel GC が応答時間要件を満たせない場合に選択
- 処理量が低下する可能性がある。
GC を 2 段階に分け、最大アプリケーション停止を抑制する方法がある。
- アプリケーションと同時に GC を実行する段階
- アプリケーションを停止して GC を実行する段階
Serial GC
Serial GC は、“Serial” という単語から分かるように「順次的な」GC 方式である。
Java SE 5、6 でのデフォルトガベージコレクタであり、主に 32 ビット JVM でシングルスレッドとして動作する。
シングルスレッドでマーク&スイープとコンパクション(Mark-Sweep-Compaction)が実行される。
シングルコア環境で利用されていた。
下の画像から分かるように、GC Thread がシングルスレッドで GC を実行するため、実行時間が長い。

つまり、GC Thread の実行中に Stop-the-World(Pause)が発生する時間が長いことを意味する。
Serial GC 関連オプションは次のとおりである。
-XX:+UseSerialGC
Parallel GC
Parallel GC は Serial GC と同じ原理で動作するが、Young 領域の GC 過程をマルチスレッドで実行する点が異なる。
そのため、GC Thread の実行は Serial GC より比較的短く、Stop-the-World(Pause)も短く発生する。

スレッド数を指定し、複数のスレッドを同時に利用して GC を実行する方式で、高速に動作する。
マルチコア環境ではデフォルトであり、マルチスレッドでマーク&スイープとコンパクションを実施する。
Low-pause 方式と Throughput 方式がある。
- Low-pause: GC を速く実行するよりも、瞬間的にアプリケーションの動作が中断される現象(pause)を最小化することに焦点を当てた方式である。
- Throughput: Minor GC の迅速な実行に焦点を当てた方式で、Full GC では Mark & Compact アルゴリズムのみを使用する。
Parallel GC 関連オプションは次のとおりである。
-XX:+UseParallelGC- この CLI オプションを使用すると、マルチスレッド young generation collector とシングルスレッド old generation collector を使用できる。
-XX:ParallelGCThreads=<desired number>- デフォルトでは、N 個の CPU を使用するホストでは parallel GC が N 個の GC スレッドを使用する。そして使用スレッド数は CLI で制御できる。
Parallel Old GC(Parallel Compacting GC)
Parallel Old GC は Java 5 update 6 から提供された GC 方式である。前述の Parallel GC と比較すると、Old 領域の GC アルゴリズムだけが異なる。この方式は Mark-Summary-Compaction 段階を経る。
Summary 段階は、GC を実行した領域について別途生きているオブジェクトを識別する点で Mark-Sweep-Compaction アルゴリズムの Sweep 段階と異なり、やや複雑な段階を経る。
Parallel Old GC 関連オプションは次のとおりである。
-XX:+UseParallelOldGC- この CLI オプションを使用すると、young generation と old generation の両方でマルチスレッド collector を使用できる。さらに compacting collector もマルチスレッドで動作する。
CMS (Concurrent Mark & Sweep, 並行マーク&スイープ) GC
CMS GC は、GC 作業をアプリケーションのスレッドと同時に実行し、GC によるアプリケーション停止(stop-the-world)を最小化することを目的とする。 ただし Compacting を実行しないため、memory をより多く消費する。
スレッド間の協調処理などに CPU リソースを使用するため、アプリケーションの処理量(throughput)低下が予想されるが、アプリケーション全体の停止時間は短くなる。その結果、GC が応答時間に与える影響は小さくなる。
CPU 使用率が高い場合に性能が低下することがあり、その場合は Parallel GC を使用する。
短所としては、CPU リソースを多く使用し、メモリ断片化が発生する可能性がある。

Initial Mark 段階で参照状態のオブジェクトを短時間で Marking した後、全停止なしに Concurrent Mark 段階で参照状態のオブジェクトを確認する。
Remark 段階で変更または追加されたオブジェクトを確認する。Concurrent Sweep 段階で参照されていないオブジェクトを整理する。
CMS GC 関連オプションは次のとおりである。
-XX:+UseConcMarkSweepGC
G1(Garbage First) GC
G1 GC は CMS を置き換えるために作られた。従来の Young、Old という領域には分けられず、ヒープを “Region” と呼ばれる小さな領域に分けて管理する。1 つ以上の Region からオブジェクトをコピーして別の Region へ移動する方式である。CMS と異なり、Compaction 段階によってメモリ断片化をなくした。Java 7 で正式に追加された。
複数 CPU と非常に大きな memory で効果的な GC を活用するためのものだ。Oracle ドキュメントによれば、heap size が 6GB より大きい場合、GC のレイテンシ(latency)を 0.5sec 以下に下げられるという。Oracle G1 GC ドキュメントによれば、Java 9 では default GC に設定されている。(それまでは Parallel GC が default)

G1 GC は Garbage だけがある Region を最初に回収するため、Garbage First という名前が付けられた。
G1 GC 関連オプションは次のとおりである。
-XX:+UseG1GC
GC の歴史
- Java 6 以前
- Serial GC, Parallel GC, CMS GC
- Java 8
- Serial GC, Parallel GC がデフォルト
- Java 7
- G1 GC が追加
- Java 9
- G1 GC がデフォルト
参考
- OpenJDK ドキュメント
- Java の GC の仕組みを整理する
- Java Garbage Collection
- Garbage Collection Algorithm and JVM Memory Management
- Java の GC はどのように動作するか?
- Java Garbage Collection
- JVM チューニング
- Getting Started with the G1 Garbage Collector
- Java Garbage Collection Basics