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 などを検討し、弱参照は「消えても困らない情報」に限定して使うのが安全。
