はじめに
前回の記事で、new したオブジェクトはヒープに置かれることを学びました。では、使い終わったオブジェクトは誰が片付けてくれるのでしょうか?
その答えが GC(ガベージコレクション) です。C言語のように free() を書かなくていいのはGCのおかげですが、「何となく動いてくれてるんでしょ」で止まっているともったいない。この記事では、GCが何をしているのかをザックリ掴みます。
対象読者: Java実務1〜3年目。GCという言葉は知っているが、中身はよくわからないエンジニア
1. GCがやっていること ― ひとことで言うと
誰からも参照されなくなったオブジェクトを見つけて、ヒープから消す
これだけです。逆に言えば、どこかから参照されている限りオブジェクトは消えません。
コードで確認
public class GcDemo {
public static void main(String[] args) {
String a = new String("Hello"); // ① オブジェクト生成、aが参照
String b = new String("World"); // ② オブジェクト生成、bが参照
a = null; // ③ "Hello"への参照がなくなる → GC対象になる
// この時点で "World" はまだbが参照しているので回収されない
}
}
2. 世代別GCの考え方
JVMのGCは「ほとんどのオブジェクトはすぐ不要になる」という経験則に基づいて設計されています。これを**世代別GC(Generational GC)**と呼びます。
ヒープの構成
| 領域 | 役割 | GCの特徴 |
|---|---|---|
| Eden |
new したオブジェクトが最初に入る場所 |
いっぱいになるとMinor GCが走る |
| Survivor (S0/S1) | Minor GCを生き延びたオブジェクトの一時置き場 | S0とS1を交互に使う |
| Old世代 | 何度もMinor GCを生き延びた長寿オブジェクト | Full GCで回収(重い処理) |
ライフサイクルの流れ
// メソッドが呼ばれるたびに一時オブジェクトが大量に作られるケース
public List<String> processData(List<Integer> numbers) {
return numbers.stream()
.map(n -> "Item-" + n) // ← ここで短命なStringが大量にnewされる
.collect(Collectors.toList());
}
-
"Item-" + nで作られるStringは Eden に入る - Edenがいっぱいになると Minor GC が走る
- もう参照されていないStringはここで回収される(大半がこれ)
- まだ使われているオブジェクトは Survivor へ移動
- Survivorで何度も生き残ったオブジェクトは Old世代 へ昇格
つまり、短命なオブジェクトはYoung世代で素早く回収され、長生きするオブジェクトだけがOld世代に行くという効率的な仕組みです。
3. GCが走ると何が起きる? ― Stop-the-World
GCが動いている間、アプリケーションのスレッドは一時停止します。これを Stop-the-World(STW) と呼びます。
Minor GCの停止時間は通常ミリ秒単位なので、ほとんどのアプリケーションでは気になりません。ただし、Full GC(Old世代の回収) は数百ミリ秒〜数秒かかることがあり、レスポンスタイムに影響します。
実務での影響例
// ありがちなパターン:キャッシュをMapで持ち続ける
private static Map<String, Object> cache = new HashMap<>();
public void addToCache(String key, Object value) {
cache.put(key, value); // staticフィールドなのでGCに回収されない
// → cacheが肥大化 → Old世代が圧迫 → Full GCが頻発
}
このようなコードは、意図せずOld世代を圧迫してFull GCの頻度を上げてしまいます。
4. GCの種類(ざっくり)
Java 11以降で使えるGCの代表的なものを紹介します。深入りはしませんが、名前だけ知っておくと調査時に役立ちます。
| GCの種類 | 特徴 | 向いている用途 |
|---|---|---|
| G1GC | Java 9以降のデフォルト。バランス型 | 一般的なWebアプリ |
| ZGC | STW時間を極端に短く抑える(1ms以下目標) | 低レイテンシが求められるシステム |
| Shenandoah | ZGCと同様の低停止時間を目指す | 同上(OpenJDK系) |
現時点では「G1GCがデフォルトで動いている」ということだけ押さえておけばOKです。
5. GCログを見てみよう(おまけ)
GCが実際にどう動いているかは、JVM起動オプションでログを出せます。
# Java 11以降
java -Xlog:gc* -jar myapp.jar
出力例(抜粋):
[0.015s][info][gc] Using G1
[1.234s][info][gc] GC(0) Pause Young (Normal) 24M->8M(256M) 3.456ms
[3.456s][info][gc] GC(1) Pause Young (Normal) 32M->10M(256M) 4.123ms
読み方:
-
Pause Young→ Minor GC(Young世代の回収) -
24M->8M→ GC前24MB使用 → GC後8MBに減った -
3.456ms→ 停止時間
「Full GCが頻繁に出ていたら要注意」と覚えておけば、実務のトラブル調査で最初の手がかりになります。
まとめ
- GCは参照されなくなったオブジェクトをヒープから消す仕組み
- ヒープは Young世代(Eden + Survivor) と Old世代 に分かれている
- ほとんどのオブジェクトはYoung世代で短命に回収される
- GC中はアプリが一時停止する(Stop-the-World)
- Full GCが頻発するとパフォーマンスに影響するので注意
次回は、GCでも回収しきれずヒープが溢れてしまった場合に起きる OutOfMemoryError の対処法を解説します。
シリーズ記事
- 第1回: ヒープとスタック
- 第2回: GC(ガベージコレクション)の基本(この記事)
- 第3回: OutOfMemoryErrorが出たらどうする?(近日公開)