概要
Full GC が発生するとアプリケーション全体が一時停止し、API のレスポンスタイムが跳ね上がったりバッチの処理時間が大幅に伸びたりします。原因の多くは、不要になったオブジェクトへの参照を保持し続ける設計にあります。キャッシュに強参照で溜め込む、コレクションをクリアせずにフィールドに持ち続ける、といったパターンは業務コードで繰り返し見かけます。この記事では、WeakReference と WeakHashMap を使った GC フレンドリーなキャッシュ、短命オブジェクト設計によるメモリ圧迫の回避、そして参照の種類(Strong / Weak / Soft / Phantom)ごとの GC 挙動の違いを整理します。どの場面でどの参照型を選ぶべきかの判断基準も示します。
使いどころ
マスタデータのインメモリキャッシュを WeakHashMap で構築し、メモリ不足時に自動で解放されるようにする
大量 CSV の行単位処理でオブジェクトをメソッドスコープに閉じ込め、Old Generation への昇格を防ぐ
画像サムネイルのキャッシュに SoftReference を使い、メモリが足りている間はキャッシュを保持しつつ、不足時には GC に回収させる
コード例
import java.util.WeakHashMap;
public class GcEfficientCache {
/** WeakHashMap を使った GC フレンドリーなキャッシュ */
static class SmartCache {
private final WeakHashMap<String, byte[]> cache = new WeakHashMap<>();
void put(String key, byte[] data) {
cache.put(key, data);
}
byte[] get(String key) {
return cache.get(key); // GC 後は null になる可能性あり
}
int size() {
return cache.size();
}
}
/** 短命オブジェクト設計: メソッドスコープで参照を閉じる */
static long processData(int count) {
var sum = 0L;
for (int i = 0; i < count; i++) {
var temp = "item-" + i; // ループ内で完結 → すぐ GC 対象
sum += temp.length();
}
return sum;
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== WeakHashMap の動作確認 ===");
var cache = new SmartCache();
byte[] data = new byte[10 * 1024 * 1024]; // 10MB
cache.put("large-data", data);
System.out.println("キャッシュサイズ(GC 前): " + cache.size());
// 強参照を切ると GC でエントリが回収される
data = null;
System.gc();
Thread.sleep(100);
System.out.println("キャッシュサイズ(GC 後): " + cache.size());
// 短命オブジェクト設計の効果を確認
System.out.println("\n=== 短命オブジェクト設計 ===");
var rt = Runtime.getRuntime();
var before = rt.totalMemory() - rt.freeMemory();
var result = processData(100_000);
System.gc();
Thread.sleep(100);
var after = rt.totalMemory() - rt.freeMemory();
System.out.println("処理結果: " + result);
System.out.println("メモリ差: " + ((after - before) / 1024) + " KB");
var bestPractices = """
=== Full GC を防ぐベストプラクティス ===
1. オブジェクトのスコープを小さく保つ
2. 大きなコレクションは適宜クリア・null 代入
3. キャッシュには WeakReference / SoftReference を活用
4. -Xmx を適切に設定(大きすぎると GC 時間増大)
5. G1GC / ZGC を使う(Java 9+ / Java 15+)
""";
System.out.println(bestPractices);
}
}Version Coverage
var による型推論で WeakReference 周りのコードが簡潔になる。record で計測結果を不変オブジェクトとして扱える。テキストブロックでベストプラクティスをドキュメント化しやすい。
// Java 17: var + record で簡潔に
var cache = new WeakHashMap<String, byte[]>();
var data = new byte[10 * 1024 * 1024];
cache.put("key", data);
data = null;
System.gc();
// record でキャッシュ状態を表現
record CacheStatus(int size, long memoryKb) {}
// 短命オブジェクト: var で簡潔に
var sum = 0L;
for (int i = 0; i < count; i++) {
var temp = "item-" + i;
sum += temp.length();
}Library Comparison
注意点
WeakHashMap のキーは WeakReference で保持されるため、キーへの強参照がなくなると GC 時にエントリが消える。リテラル文字列をキーにすると String Pool に強参照が残り、期待どおりに回収されない
SoftReference は「メモリ不足時」に回収されるが、そのタイミングは JVM 依存。キャッシュのヒット率を保証するものではない
WeakReference / SoftReference を使うと get() が null を返す可能性があるため、呼び出し側で必ず null チェックが必要になる
コレクションを null 代入するだけでは、中の要素への参照が他に残っていれば GC されない。参照グラフ全体を意識すること
FAQ
キャッシュには SoftReference が適しています。メモリ不足時のみ回収されるため、ヒット率が保たれます。WeakReference は次の GC で即回収されるため、一時的な参照の追跡に向いています。
推奨しません。リテラル文字列は String Pool に強参照が残るため、GC で回収されず WeakHashMap の利点が失われます。new String() でインスタンスを作るか、別の型をキーにしてください。
GC ログ(-Xlog:gc*)を有効にし、Full GC の出現頻度と停止時間を確認します。VisualVM や GCViewer でログをグラフ化すると傾向がつかみやすくなります。