java-recipes

ホーム ガベージコレクション(GC) › GC-03

GC-03: FullGC を避けるメモリ効率設計

FullGC が発生するとアプリが一時停止(Stop-The-World)します。この停止を最小限に抑えるための コード設計パターンを学びましょう。WeakReferenceSoftReference・ 短命オブジェクト設計など、実践的な手法を解説します。

いつ使うか

  • バッチ処理で大量データを扱い、途中でメモリが枯渇しそうなとき
  • メモリを使うキャッシュを作りたいが、OOM は避けたいとき
  • 長時間稼働するサーバーアプリでメモリリークを防ぎたいとき
  • GC の停止時間を計測したら想定以上に長かったとき

参照の種類と GC の挙動

参照の種類クラスGC での挙動
強参照(Strong)通常の変数代入参照がある限り回収されない
弱参照(Weak)WeakReference次の GC で回収される
軟参照(Soft)SoftReferenceメモリ不足時のみ回収(キャッシュ向き)
ファントム参照PhantomReferenceGC 後の後処理用(上級者向け)

サンプルコード

GcEfficiencySample.java
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.WeakHashMap;

public class GcEfficiencySample {

    // アンチパターン: 不要な参照を保持し続ける
    static class BadCache {
        private final List<byte[]> cache = new ArrayList<>();

        void addData(byte[] data) {
            cache.add(data); // 追加するだけで削除しない → 旧世代に蓄積
        }
    }

    // WeakReference: 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) {
        long sum = 0;
        for (int i = 0; i < count; i++) {
            // ループ内のオブジェクトはすぐ GC 対象 → Young Generation で回収
            String temp = "item-" + i;
            sum += temp.length();
        }
        return sum; // temp は全てスコープを外れ GC 可能
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== FullGC を避けるメモリ設計 ===");

        // WeakReference の動作確認
        SmartCache cache = new SmartCache();
        byte[] data = new byte[10 * 1024 * 1024]; // 10MB
        cache.put("large-data", data);
        System.out.println("キャッシュサイズ(GC前): " + cache.size());

        // 強参照を切る
        data = null;
        System.gc();
        Thread.sleep(100);
        System.out.println("キャッシュサイズ(GC後): " + cache.size()); // 0 になる可能性

        // 短命オブジェクト設計
        System.out.println("\n=== 短命オブジェクト設計 ===");
        Runtime rt = Runtime.getRuntime();
        long before = rt.totalMemory() - rt.freeMemory();
        long result = processData(100000);
        System.gc();
        Thread.sleep(100);
        long after = rt.totalMemory() - rt.freeMemory();
        System.out.println("処理結果: " + result);
        System.out.println("メモリ差: " + ((after - before) / 1024) + " KB(短命オブジェクトは回収済み)");

        System.out.println("\n=== FullGC を防ぐベストプラクティス ===");
        System.out.println("1. オブジェクトのスコープを小さく保つ");
        System.out.println("2. 大きなコレクションは適宜クリア・null 代入");
        System.out.println("3. キャッシュには WeakReference / SoftReference を活用");
        System.out.println("4. -Xmx を適切に設定(大きすぎると GC 時間増大)");
        System.out.println("5. G1GC / ZGC を使う(Java 9+/15+)");
    }
}

WeakHashMap のキーは WeakReference で保持されています。強参照がなくなると GC に回収され、マップからも自動的に除去されます。キャッシュとして活用すると、メモリ不足時に自動解放される安全なキャッシュになります。

よくあるミス・注意点

WeakReference は null になる可能性を常に考慮する

weakRef.get() は GC が発生した後に null を返すことがあります。 取得後は必ず null チェックをしてから使いましょう。 null のまま使うと NullPointerException が発生します。

static コレクションへの追加は Old Generation を圧迫する

static フィールドのリストやマップに追加し続けると、 オブジェクトが永続的に参照され続けて Old Generation に移動します。 Old Generation がいっぱいになると FullGC が発生します。 定期的なクリアや最大サイズの制御を忘れないようにしましょう。

内部クラスは外側のクラスへの参照を持つ

非静的な内部クラス(匿名クラスを含む)は、自動的に外側のクラスのインスタンスへの参照を保持します。 内部クラスのオブジェクトが長命である場合、外側のオブジェクトも GC されなくなります。 long-lived な内部クラスには static 内部クラスを使いましょう。

テストする観点

  • WeakHashMap に格納したデータは、 強参照を null にして System.gc() を呼ぶとサイズが減ること
  • メソッドスコープ内で作成した大量のオブジェクトは、メソッド終了後に GC で回収されること
  • WeakReference.get() は GC 後に null を返すことがあるため、null チェックが必要なこと(境界値テスト)
  • static フィールドに保持したオブジェクトは GC されないこと(メモリリーク確認)

GitHub でソースコードを見る →