Java 逆引き集 | WeakHashMap の利用ケース(キャッシュと GC) — メモリリーク防止

Java Java
スポンサーリンク

WeakHashMap の利用ケース(キャッシュと GC) — メモリリーク防止

WeakHashMap は「キーを弱参照で保持する Map」。キーへの強参照が外部で途切れると、GC(ガベージコレクション)後にエントリが自動的に消えます。これにより「参照がなくなったオブジェクトに紐づく付帯情報」を残さず、メモリリークを防ぎやすくなります。初心者にも分かりやすく、使いどころ・注意点・コード例をかみ砕いて解説します。


仕組みと使いどころ

  • 基本仕組み:
    • キーは弱参照: WeakHashMap はキーに対して弱参照を使います。キーへの強参照がなくなると、GC後にそのキー(と値)はマップから自動削除されます。
    • 値は強参照: 値は通常の強参照です(値だけを弱くしたいわけではありません)。
  • 向いている場面:
    • 付帯情報の添付: 任意オブジェクトに「メタ情報」や「キャッシュ済み計算結果」を紐付けたいが、そのオブジェクトが不要になったら関連情報も消えてほしい。
    • ライフサイクル追従: UIコンポーネント、ドメインオブジェクト、リフレクションの Class/Method などに付随する一時的データ。
    • メモリリーク防止: 長生き Map が、短命オブジェクトの情報を握り続ける事態を避ける。
  • 向かない場面:
    • 一般的なキャッシュの保持: GCのタイミング次第で「欲しいタイミングで消える」保証がないため、LRU/TLRU/サイズ制限などの明確なポリシーが必要なキャッシュには不向き。
    • キーが永続的に必要: キーを確実に残したい場合は HashMap/ConcurrentHashMap を使う。

基本コード例(キーの強参照が切れたら消える)

import java.util.Map;
import java.util.WeakHashMap;

public class WeakMapBasics {
    static class Key {
        final String id;
        Key(String id) { this.id = id; }
        @Override public String toString() { return id; }
        // equals/hashCode を適切に実装するのが望ましい(参照同一性で良い用途もある)
    }

    public static void main(String[] args) throws Exception {
        Map<Key, String> map = new WeakHashMap<>();

        Key k1 = new Key("A");
        Key k2 = new Key("B");

        map.put(k1, "meta-A");
        map.put(k2, "meta-B");

        System.out.println("初期: " + map); // {A=meta-A, B=meta-B}(表示順は未保証)

        // k1への強参照を切る
        k1 = null;

        // GC で弱参照キーが回収される可能性(タイミングはJVM次第)
        System.gc();
        Thread.sleep(50); // ほんの少し待つ(デモ用)

        System.out.println("GC後: " + map); // {B=meta-B} になる可能性が高い
    }
}
Java
  • ポイント: キーの強参照が切れると、GC後にマップからも消える。デモの挙動は JVM/タイミングに依存します。

よくあるユースケース

  • オブジェクト→付帯情報の辞書:
    • Label: 画像オブジェクトに計算済みサムネイル、エンティティに一時バリデーション結果など。
    • 効果: 元オブジェクトが捨てられれば付帯情報も自然に消える。
  • リフレクションのメタキャッシュ:
    • Label: Class や Method に対する解析結果(アノテーション読み取り、フィールド一覧)。
    • 効果: クラスローダのライフサイクルに合わせて不要分が回収されやすくなる。
  • 短命キーの一時マップ:
    • Label: 処理中だけ存在するキーに紐付くワーク領域。
    • 効果: 処理終了後にキーが消えると、マップも自動的に軽くなる。

注意点と落とし穴

  • キャッシュ用途の誤用:
    • 注意: 「使い回したいが、GCで消えたくない」という一般的キャッシュには不向き。再利用前に消えることがある。
    • 代替: サイズ制御や順序ポリシーが必要なら LinkedHashMap LRU、期限付きなら自前管理や外部ライブラリ(Caffeine など)、並行環境なら ConcurrentHashMap+原子操作。
  • キーの実装:
    • 注意: equals/hashCode を適切に。キーの同値性が不安定だと取り違えや重複が起きる。
    • コツ: 不変なキー(内容が変わらない)を使う。
  • 値は回収されない場合がある:
    • 注意: 値に他から強参照が残っている場合、値は当然残る。WeakHashMap が弱いのはキーだけ。
  • GCタイミング依存:
    • 注意: いつ消えるかは JVM 任せ。即時性はない(System.gc() はヒント程度)。
    • コツ: 「消えても困らない情報」だけを載せる。
  • スレッド安全性:
    • 注意: WeakHashMap は非同期。マルチスレッドでは外部同期か、用途に応じて別の構造(ConcurrentHashMap など)にする。

キャッシュ設計の比較と指針

  • WeakHashMap(弱キー):
    • 長所: キーのライフサイクルに追従、リーク防止。
    • 短所: 再利用前に消える可能性、ポリシー制御不可。
  • SoftReference(ソフト参照)+マップ:
    • 長所: メモリ逼迫時のみ回収されやすい。キャッシュに向く。
    • 短所: 実装が少し複雑、値側に SoftReference を巻く必要。
  • LinkedHashMap LRU(removeEldestEntry):
    • 長所: サイズ上限や最近使用優先が明確。
    • 短所: キーの参照状態とは独立、手動ポリシー設計が必要。
  • Caffeine 等のライブラリ:
    • 長所: LRU/LFU、TTL、最大サイズ、統計など充実。
    • 短所: 依存が増える。標準のみで完結しない。
  • ConcurrentHashMap+原子操作:
    • 長所: 高並行性のキャッシュ・メモ化に向く(computeIfAbsent 等)。
    • 短所: サイズや期限の制御は自前。

例題で理解する

例題1: オブジェクトに付帯メタを添付(弱キーでリーク防止)

import java.util.Map;
import java.util.WeakHashMap;

class Meta {
    final String info;
    Meta(String info) { this.info = info; }
}
class Doc {
    final String id;
    Doc(String id) { this.id = id; }
    @Override public String toString() { return "Doc(" + id + ")"; }
}

public class WeakMetaStore {
    private final Map<Doc, Meta> metas = new WeakHashMap<>();

    public void attach(Doc d, String info) {
        metas.put(d, new Meta(info));
    }
    public Meta get(Doc d) {
        return metas.get(d); // dへの強参照が残っていれば取得できる
    }

    public static void main(String[] args) throws Exception {
        WeakMetaStore store = new WeakMetaStore();
        Doc d1 = new Doc("A");
        store.attach(d1, "parsed");

        System.out.println(store.get(d1).info); // parsed

        d1 = null;           // Docへの強参照を切る
        System.gc();         // GCヒント
        Thread.sleep(50);

        System.out.println("サイズ推定: " + store.metas.size()); // 0 になる可能性
    }
}
Java

例題2: リフレクションメタキャッシュ(Class→解析結果)

import java.util.Map;
import java.util.WeakHashMap;

class Analysis { /* 重い解析結果 */ }

public class ReflectCache {
    private final Map<Class<?>, Analysis> cache = new WeakHashMap<>();

    public Analysis analyze(Class<?> c) {
        return cache.computeIfAbsent(c, this::heavyAnalysis);
    }

    private Analysis heavyAnalysis(Class<?> c) {
        // アノテーション走査などの重い処理を想定
        return new Analysis();
    }

    public static void main(String[] args) {
        ReflectCache rc = new ReflectCache();
        Analysis a1 = rc.analyze(String.class);
        Analysis a2 = rc.analyze(String.class); // 既存を再利用(GCで消えてなければ)

        System.out.println(a1 == a2); // true の可能性(未回収なら)
    }
}
Java

テンプレート集

  • 弱キー辞書の宣言
Map<K, V> weak = new java.util.WeakHashMap<>();
Java
  • 付帯情報の添付(存在時再利用)
V v = weak.computeIfAbsent(key, k -> buildValueFor(k));
Java
  • 存在確認・取り出し
boolean has = weak.containsKey(key);
V val = weak.get(key);
Java
  • サイズ・走査(参考程度)
for (var e : weak.entrySet()) {
    // GC後に消えることがあるため、管理目的には過信しない
}
Java

実務でのコツ

  • 「消えてもOK」な付帯情報だけを載せる: Coreデータは別の堅牢なストアへ。
  • キーのライフサイクルに依存する設計: キーが長生きなら WeakHashMap の恩恵は小さい。
  • キャッシュ要件を明確化: サイズ上限・期限・再計算コストなどが重要なら、WeakHashMap以外を選定。
  • テストで GC 依存を避ける: WeakHashMap の挙動テストは brittle になりがち。ロジックは「消えても再計算する」ことを前提に。

まとめ

  • WeakHashMap は「キーが弱参照」の Map。キーへの強参照が切れれば、GC後にエントリが自動削除され、メモリリーク防止に役立つ。
  • 一般的なキャッシュとしては不向き。付帯情報の添付や、ライフサイクル連動の一時辞書に向いている。
  • キャッシュ要件がある場合は LRU(LinkedHashMap)や SoftReference、Caffeine、ConcurrentHashMap などを検討し、弱参照は「消えても困らない情報」に限定して使うのが安全。
タイトルとURLをコピーしました