概要

Full GC が発生するとアプリケーション全体が一時停止し、API のレスポンスタイムが跳ね上がったりバッチの処理時間が大幅に伸びたりします。原因の多くは、不要になったオブジェクトへの参照を保持し続ける設計にあります。キャッシュに強参照で溜め込む、コレクションをクリアせずにフィールドに持ち続ける、といったパターンは業務コードで繰り返し見かけます。この記事では、WeakReference と WeakHashMap を使った GC フレンドリーなキャッシュ、短命オブジェクト設計によるメモリ圧迫の回避、そして参照の種類(Strong / Weak / Soft / Phantom)ごとの GC 挙動の違いを整理します。どの場面でどの参照型を選ぶべきかの判断基準も示します。

使いどころ

マスタデータのインメモリキャッシュを WeakHashMap で構築し、メモリ不足時に自動で解放されるようにする

大量 CSV の行単位処理でオブジェクトをメソッドスコープに閉じ込め、Old Generation への昇格を防ぐ

画像サムネイルのキャッシュに SoftReference を使い、メモリが足りている間はキャッシュを保持しつつ、不足時には GC に回収させる

コード例

GcEfficientCache.java
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);
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var による型推論で WeakReference 周りのコードが簡潔になる。record で計測結果を不変オブジェクトとして扱える。テキストブロックでベストプラクティスをドキュメント化しやすい。

Java 17
// 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

標準 API(WeakHashMap / SoftReference)少量のキャッシュや参照管理で十分なとき。外部依存なしで GC フレンドリーな設計が可能。キャッシュの最大サイズ制御や TTL(有効期限)は自前で実装する必要がある。
CaffeineLRU / TTL / サイズ制限付きの本格的なキャッシュが必要なとき。高負荷環境でのスループットに優れる。外部依存が増える。少量データのキャッシュにはオーバースペック。
Guava CacheCaffeine ほどの性能は不要だが、CacheBuilder の宣言的な API で手軽にキャッシュを構築したいとき。Caffeine に比べて性能面で劣る。Guava 全体の依存を持ち込むことになる。

注意点

WeakHashMap のキーは WeakReference で保持されるため、キーへの強参照がなくなると GC 時にエントリが消える。リテラル文字列をキーにすると String Pool に強参照が残り、期待どおりに回収されない

SoftReference は「メモリ不足時」に回収されるが、そのタイミングは JVM 依存。キャッシュのヒット率を保証するものではない

WeakReference / SoftReference を使うと get() が null を返す可能性があるため、呼び出し側で必ず null チェックが必要になる

コレクションを null 代入するだけでは、中の要素への参照が他に残っていれば GC されない。参照グラフ全体を意識すること

FAQ

WeakReference と SoftReference はどう使い分けますか。

キャッシュには SoftReference が適しています。メモリ不足時のみ回収されるため、ヒット率が保たれます。WeakReference は次の GC で即回収されるため、一時的な参照の追跡に向いています。

WeakHashMap のキーにリテラル文字列を使っても大丈夫ですか。

推奨しません。リテラル文字列は String Pool に強参照が残るため、GC で回収されず WeakHashMap の利点が失われます。new String() でインスタンスを作るか、別の型をキーにしてください。

Full GC が頻発しているかどうかはどう調べますか。

GC ログ(-Xlog:gc*)を有効にし、Full GC の出現頻度と停止時間を確認します。VisualVM や GCViewer でログをグラフ化すると傾向がつかみやすくなります。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます