1. GC 入門
C や C++ を書いたことがあるなら、free() や delete で手動でメモリ解放を行う必要があるのをご存じでしょう。Java はもっと簡単です。new でオブジェクトを作るだけで、削除は不要です—専用の「掃除屋」であるガベージコレクタ(Garbage Collector、GC)が担当します。
GC は JVM の一部で、参照がなくなったオブジェクトの占有メモリを自動で解放します。これにより、Java 開発者はメモリ解放忘れ(リーク)や、逆にまだ必要なオブジェクトを誤って削除してクラッシュ、という心配をしなくて済みます。
しかしどんな掃除屋も完璧ではありません。GC が最悪のタイミングで「大掃除」(Stop-the-World)を始めてしまったり、期待ほど速く動かないこともあります。そこで JVM には複数の実装が用意されており、適切なものを選ぶことでアプリの性能に大きな差が生まれます。
主なガベージコレクタの種類
Serial GC
- Serial GC — 最もシンプルで古いガベージコレクタ。
- 単一スレッドで動作。
- 回収中は他のスレッドをすべて停止(Stop-the-World)。
- マルチスレッドを多用しない小規模アプリに適する。
- 有効化フラグ: -XX:+UseSerialGC
Parallel GC
- Parallel GC(別名「Throughput Collector」)。
- 複数スレッドで回収を実行。
- 最大スループットを重視。
- Stop-the-World の停止はあるが、Serial より高速。
- 短い停止が許容されるサーバー向けアプリに適する。
- 有効化フラグ: -XX:+UseParallelGC
CMS (Concurrent Mark Sweep)
- CMS — 既に古いが長く使われた、停止時間を抑える GC。
- アプリと部分的に並行して動き、停止時間を短縮。
- 設定がやや複雑でオーバーヘッドもある。
- Java 9 で廃止予定(deprecated)とマーク。
- 有効化フラグ: -XX:+UseConcMarkSweepGC
G1 (Garbage First)
- G1 GC — 最新のデフォルトコレクタ(Java 9 以降)。
- 停止時間の最小化と性能のバランスを取る。
- ヒープを多数の小さなリージョンに分割(リージョン型モデル)。
- ヒープ全体ではなく、リージョンを選択的に回収できる。
- 目標とする最大停止時間を指定可能(例: -XX:MaxGCPauseMillis=200)。
- 有効化フラグ: -XX:+UseG1GC(通常は不要。G1 がデフォルト)。
ZGC と Shenandoah
- ZGC と Shenandoah — 近代的な低遅延 GC。
- 目的は最小の停止(ミリ秒単位)、巨大なヒープ(テラバイト級)でも。
- アプリとほぼ完全に並行して動作。
- 要件: Java 11+(ZGC)または Java 12+(Shenandoah)。
- レイテンシに厳しいシステム(取引所、フィンテック、リアルタイム分析)に適する。
- 有効化フラグ: -XX:+UseZGC または -XX:+UseShenandoahGC
3. 現代的 GC の動作原理
Young/Old 世代(Young/Old Generation)
JVM はヒープを大きく二つに分けます:
Young 世代(Young Generation): 新しく生成されたオブジェクトが入ります。ここは回収頻度が高く、処理も速い(Minor GC)。
Old 世代(Old Generation, Tenured): Young で複数回の回収を生き延びたオブジェクトが昇格してきます。回収頻度は低いが、時間は長い(Major/Full GC)。
なぜこうするのか。Java の多くのオブジェクトは非常に短命(例: 一時的な文字列やメソッド内のコレクション)だからです。Young の回収を頻繁かつ高速に行い、Old を触らずに済ませられます。
Minor GC
- Young 世代のみを回収。
- 高速で停止は短い。
- Old のオブジェクトは対象外。
Major (Full) GC
- ヒープ全体(Young と Old)を回収。
- 時間がかかることがある(巨大ヒープでは秒以上)。
- 通常は長いアプリ停止を伴う。
GC はどのオブジェクトを削除すべきかをどう判断するか?
GC はコアとなる参照集合(root set。スレッドのスタック上のローカル変数、static フィールド、メソッド引数など)から「生きている」オブジェクトを探索します。到達可能なものは生存、そうでないものはゴミです。
4. 現代的 GC の比較: G1、ZGC、Shenandoah
最新で広く使われる GC の違いを整理します。以下の表をご覧ください。
| コレクタ | 主目的 | メモリモデル | 最小停止時間 | スケーラビリティ | サポート | 適用シナリオ |
|---|---|---|---|---|---|---|
| G1 | 停止時間と速度のバランス | リージョン | 約10〜200ミリ秒 | 数百GBまで | Java 9+(デフォルト) | 大半のサーバーアプリケーション |
| ZGC | 最小停止 | リージョン、カラーリング | <10ミリ秒 | テラバイト級まで | Java 11+ | リアルタイム、レイテンシクリティカル |
| Shenandoah | 最小停止 | リージョン、カラーリング | <10ミリ秒 | テラバイト級まで | Java 12+(RedHat) | リアルタイム、レイテンシクリティカル |
G1 GC: Garbage First
- ヒープを多数のリージョン(通常 1〜32MB)に分割。
- 回収時にゴミの多いリージョンを優先して選択(garbage first)。
- ヒープ全体ではなく一部のみを回収可能。
- 目標停止時間を指定可能: -XX:MaxGCPauseMillis=200。
- 速度と停止のバランスに優れ、Java 9 以降デフォルト。
有効化の例(無効化されている場合):
java -XX:+UseG1GC -jar myapp.jar
ZGC: Z Garbage Collector
- Java 11 では実験的、Java 15 で安定化。
- アプリをほとんど停止させない: ヒープが 1〜2TB でも停止は通常 <10ミリ秒。
- カラーリング(coloring)と特殊ポインタを使用。
- 64bit の JVM が必要。32bit システムでは動作しない。
- Linux、macOS、Windows をサポート。
有効化の例:
java -XX:+UseZGC -jar myapp.jar
Shenandoah
- RedHat によって開発。目標は ZGC と近い。
- 最小停止で、アプリと積極的に並行動作。
- Linux と Windows をサポート。OpenJDK の一部ビルドに含まれる。
- 類似の技術を用いるが、内部アルゴリズムは異なる。
有効化の例:
java -XX:+UseShenandoahGC -jar myapp.jar
ビジュアル比較
graph TD
A[Young世代] -->|Minor GC| B[Old世代]
B -->|Major GC| C[GC Pause]
D[G1: リージョン] --> E[選択的リージョン]
F[ZGC/Shenandoah: リージョン] --> G[並行回収]
5. 実践: 使用中の GC を確認・変更する方法
どの GC が使われているかを確認する方法
- JVM ログ: アプリを -Xlog:gc*(Java 9+)または -verbose:gc(Java 8 以前)で起動します。 ログに、使用中の GC や停止の頻度が表示されます。
- jcmd: 次を実行:
ここで <pid> は Java プロセスの識別子(PID)です。jcmd <pid> VM.flags - jvisualvm: 「Monitoring」セクションで GC の種類を確認できます。
アプリで GC を変更するには?
Java プログラムの起動時に必要なフラグを付けます:
G1 GC(デフォルト。明示も可):
java -XX:+UseG1GC -jar myapp.jar
ZGC:
java -XX:+UseZGC -jar myapp.jar
Shenandoah:
java -XX:+UseShenandoahGC -jar myapp.jar
ヒープサイズと停止時間の設定
- 最大ヒープサイズ: -Xmx2G
- 最小ヒープサイズ: -Xms512M
- G1 の目標停止時間: -XX:MaxGCPauseMillis=200
起動例(フル):
java -Xms512M -Xmx2G -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar myapp.jar
6. 用途別 GC 選定の要点
G1 を選ぶべきとき
- 多くのサーバー/デスクトップアプリにおけるデフォルト選択として優秀。
- ヒープが数百MB〜数百GBの範囲で良好に動作。
- 速度と停止のバランスが良い。
ZGC または Shenandoah を選ぶべきとき
- アプリが遅延に敏感(レイテンシクリティカル: 取引所、オンラインゲーム、リアルタイム分析)。
- ヒープが非常に大きい(数百GB以上)。
- 許容される停止がミリ秒レベルのみ。
- 要件: Java 11+(ZGC)または Java 12+(Shenandoah)。
Parallel GC で十分なとき
- スループット重視で、停止が致命的でない小規模アプリ。
- 停止(Full GC)を「やり過ごせる」バッチ処理。
7. 例: シンプルなアプリでの GC 挙動比較
大量の一時オブジェクトを生成する小さなアプリ(注文処理のシミュレーション):
public class GCSimulator {
public static void main(String[] args) {
while (true) {
// 毎ループで 100,000 個のオブジェクトを作成
for (int i = 0; i < 100_000; i++) {
String s = new String("Order-" + i);
}
// 少し休む
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}
}
異なる GC で起動してログを確認する:
java -Xmx256M -XX:+UseG1GC -Xlog:gc* GCSimulator
java -Xmx256M -XX:+UseZGC -Xlog:gc* GCSimulator
何が起きるか?
G1 は短い停止を高頻度で行います。ZGC/Shenandoah は停止がさらに短い一方で、発生頻度は高くなる場合があります。Parallel GC は停止が長いものの頻度は低めです。
8. GC 利用時の典型的な落とし穴と注意点
誤り1: GC がメモリ問題をすべて解決してくれると思い込む。 GC は魔法の杖ではありません。不要なオブジェクトへの参照を保持し続ければ、どんな GC でもメモリリークは避けられません。
誤り2: System.gc() を強制呼び出しする。 JVM はいつ回収すべきかを自動的に最適化しています。強制 GC は長い停止を招き、性能を落とすことがあります。
誤り3: GC ログを無視する。 GC ログを見ないと、アプリが定期的に Full GC で「固まっている」事実に気付けないことがあります。
誤り4: 古い GC を使い続ける。 たとえば CMS は既に開発されていません。G1 や最新の低遅延コレクタへ移行しましょう。
誤り5: 用途に合わない GC を選ぶ。 レイテンシクリティカルなアプリで Parallel GC を選べば長い停止を招きます。バッチ処理で ZGC を有効にすれば余計なオーバーヘッドになります。
GO TO FULL VERSION