目次
GC と「到達可能性」
JS のメモリ管理は GC に任されている
JavaScript では、C++ のように delete したり free する必要はありません。
「到達不能になったオブジェクトは GC により自動解放される」のが基本です。
「到達可能(reachable)」とは:
- グローバル変数
- ローカル変数(スタック上の参照)
- それらから辿れるオブジェクト
上記から辿れないもの(グラフから切り離されたノード)は到達不能により、GC の対象になる
「強参照」的なふるまい
通常の参照は、すべて「強参照(strong reference)」とみなせます:
let obj = { value: 123 };
// どこかに参照がある限り GC は解放しないobjがスコープから外れたり、他にも誰も参照していない状態になったとき、初めて GC される可能性が出る- Map/Array などのコレクションに入れている間も 到達可能 とみなされるので、GC は解放しない
この「コレクションに入れられたせいで GC されず、メモリリークする」という問題が出るケースに対して導入されたのが
WeakMap/WeakSet(キー / 要素が弱い参照)- さらに細かい制御のための
WeakRef/FinalizationRegistry
という流れです。
弱参照とは何か
弱参照の定義
「そこにオブジェクトがいてもいなくてもいい という形の参照」
- GC の観点からは「その参照は“生存の理由”には数えない」
- 他に強参照が一つもなければ、GC はオブジェクトを自由に解放してよい
- 解放されたあと、弱参照からは もうそのオブジェクトにアクセスできない(
null相当が返るイメージ)
なぜ必要になるのか
- キャッシュ
- オブジェクトAに対して重い計算結果をキャッシュしたい
- しかし「キャッシュがあるがためにAが永遠に解放されない」のは困る
- 外部リソースとの対応表
- DOMノード⇔ラッパオブジェクト、画像オブジェクト⇔メタ情報
- DOMノードが GC されたら、ラッパ側やメタ情報も自動的に「もう使わなくていい扱い」にしたい
「“メモリが足りなくなったら、勝手にキャッシュを捨てていい”タイプの情報」との相性が良いです。
WeakRef の基本
WeakRef の構造と挙動
WeakRef は、単一オブジェクトへの弱い参照を表すクラスです。
const obj = { value: 123 };
const ref = new WeakRef(obj);
// どこかで GC が obj を回収するかもしれない
//(他の強参照が全てなくなった後)new WeakRef(target):target への弱参照を作るref.deref():今も target が生きていればその参照を返す。
既に GC 済みならundefinedを返す。
let obj = { name: "Taro" };
const ref = new WeakRef(obj);
console.log(ref.deref()); // { name: "Taro" }(まだ生きている)
obj = null; // 強参照を捨てる → いつか GC されうる状態
// しばらくして…
const value = ref.deref();
if (value) {
console.log("まだいる:", value.name);
} else {
console.log("もうGCされたっぽい");
}重要ポイント:
- いつ GC されるかは完全にランタイム任せで、タイミングは一切保証されない
- GC が走らなければ、ずっと
deref()できるかもしれない - 一度 GC されたら
deref()は永遠にundefinedになる
つまり WeakRef は、
「たまたままだ残っていれば使う、なければ諦める」タイプのキャッシュ・最適化のための仕組み
として設計されています。
WeakRef の注意点
- 正しさ(correctness)のために使ってはいけない
- 「
deref()で必ずオブジェクトが取れる前提」
→ この前提は成り立たない - 例:ユーザーデータを WeakRef にしか持たない → タイミングによって消える → バグ
- 「
- ロジック上「消えたら困る値」には使わない
- ログイン中ユーザー・永続データ・UI状態などは弱参照ではなく普通の参照を使うべき
- WeakRef はあくまで “あればラッキー”なキャッシュ 専用のイメージ
- 強参照がどこかに残っていれば、弱参照の意味はない
const obj = { a: 1 }; const ref = new WeakRef(obj); const cache = obj; // ← これが強参照// この状態では GC は obj を解放しないので、WeakRef にした意味がほぼない
簡単な WeakRef キャッシュのイメージ
class ExpensiveCalculator {
constructor() {
this._cache = new Map(); // key -> WeakRef(result)
}
calculate(key) {
const ref = this._cache.get(key);
const cached = ref?.deref();
if (cached) {
// まだキャッシュが生きている
return cached;
}
// 重い計算(仮)
const result = { value: key * 2 };
// 弱参照としてキャッシュに入れる
this._cache.set(key, new WeakRef(result));
return result;
}
}- メモリに余裕がある間はキャッシュが効く
- メモリが逼迫して GC が走れば、キャッシュは勝手に消える(
deref()が undefined になる)
このように 「メモリミスで動作が変わっても構わない最適化」 のために使うと、設計として綺麗にまとまります。
FinalizationRegistry の基本
何をするためのAPIか
FinalizationRegistry は、「あるオブジェクトが GC された“あとで”、コールバックを実行してもらう」ための仕組みです。
const registry = new FinalizationRegistry((heldValue) => {
console.log("GCされたオブジェクトに対応する値:", heldValue);
});- コンストラクタに「ファイナライザ関数」を渡す
register(target, heldValue, [unregisterToken])で監視対象を登録targetが GC されると、いつか レジストリに渡した関数が呼ばれる- 第1引数の
heldValueに、対応する値が渡される(target そのものではない点に注意)
簡単な使用例
const registry = new FinalizationRegistry((heldValue) => {
console.log("クリーンアップ:", heldValue);
});
(function () {
const obj = { id: 1 };
// obj がGCされたときに "object #1" をログに出したい
registry.register(obj, "object #1");
})();- 即時関数内で作られた
objは、関数を抜けたあと他に参照がなければ GC 対象 - GC によって解放された「いつか」のタイミングで、コールバックが呼び出される可能性がある
FinalizationRegistry の制約・注意点
- 実行タイミングは一切保証されない
- いつGCされるか → 不定
- GCされたあと、いつコールバックが走るか → さらに不定
- 極端な話、「プロセス終了まで一度も呼ばれない」こともある
- 実行されること自体が保証されない
- プロセスが途中で終了すれば、当然コールバックは走らない
- したがって、これを“後始末の唯一の手段”にしてはいけない
- ビジネスロジックに使ってはいけない
- 例:DBからのレコード削除、ログ保存、ネットワーク通信など
- それらは アプリケーション側で明示的に実行 すべきで、GC 任せにするのは危険
- パフォーマンス上のオーバーヘッドや複雑さ
- GC のたびにファイナライザを管理する必要があるため、乱用するとパフォーマンスが落ちる
- 一般的なライブラリや業務コードでは「必要な場面以外は使わない」のが無難
WeakRef と FinalizationRegistry の組み合わせ例
リソース付きオブジェクトの自動クリーンアップ
たとえば:
- あるオブジェクトが「ネイティブリソース(ファイルハンドル、ソケット等)」を握っている
- 通常は
close()で明示的にクローズする - しかし、万が一ユーザーが
close()を呼び忘れた場合にも
「最後の保険」としてGCタイミングでクローズしたい
というパターン。
class ResourceWrapper {
constructor(resource) {
this.resource = resource;
registry.register(this, resource.id);
}
close() {
if (this.resource) {
this.resource.close();
this.resource = null;
registry.unregister(this); // 明示クローズしたので監視解除
}
}
}
const registry = new FinalizationRegistry((id) => {
console.warn(`Resource ${id} のクローズし忘れ検知(最終クリーンアップ候補)`);
// ここで OS レベルの解放処理などを呼ぶケースもあるが、
// ネットワーク通信などは極力避けるべき
});- 正しいパターン:
- メインはあくまで明示的な
close() - FinalizationRegistry は「デバッグ補助」や「緊急時の保険」に留める
- メインはあくまで明示的な
WeakRef と組み合わせたキャッシュ+クリーンアップ
class Cache {
constructor() {
this._map = new Map(); // key -> WeakRef(value)
this._registry = new FinalizationRegistry((key) => {
// value(GC済み)に対応する key が heldValue として渡ってくる想定
this._map.delete(key);
});
}
set(key, value) {
this._map.set(key, new WeakRef(value));
this._registry.register(value, key);
}
get(key) {
const ref = this._map.get(key);
const value = ref?.deref();
if (!value) {
this._map.delete(key); // 中身が消えていたらエントリも消す
}
return value;
}
}- メモリ不足時には、value が GC される
- GC の後で FinalizationRegistry が呼ばれ、key → WeakRef のペアも片付けてくれる
ただし、ここでも以下が成立します:
- 「キャッシュがこまめにクリーンアップされれば嬉しいが、クリーンアップが多少遅れても致命的ではない」
⇒ だからこそ FinalizationRegistry が“ギリ許される”
WeakMap / WeakSet との関係
WeakMap:- キーが弱参照(キーが到達不能になれば、そのエントリは自動解放されうる)
- キーはオブジェクトのみ
WeakSet:- 要素が弱参照
- 要素はオブジェクトのみ
WeakRef:- 「単一オブジェクトへの弱いポインタ」を自前で管理する仕組み
FinalizationRegistry:- オブジェクトが GC されたことを「あとから知る」仕組み
設計の順序としては:
- まず
WeakMap/WeakSetで解決できないかを考える - それでも足りず、かつ GC 依存の動作が本当に必要なときに
WeakRef/FinalizationRegistryを検討する
くらいの慎重さでちょうど良いです。
いつ使うべきか?使うべきでないか?
使う“かもしれない”場面
- かなり大規模なアプリ・ライブラリで
- 「多数のオブジェクトに紐づくキャッシュ」
- 「薄いラッパオブジェクト(ViewModel など)のライフサイクル管理」
- パフォーマンスチューニング段階で、
- 通常の strong ref / WeakMap だけではメモリが厳しいと判明し、「キャッシュをもっと積極的にGCに委ねたい」事情が生じたとき
ほとんどのアプリでやってはいけないこと
- ビジネスロジックを WeakRef / FinalizationRegistry に依存させる
- 例:ユーザー認証状態やセッションを弱参照だけで管理する
- 例:GCに任せてDBレコードを削除する
- 正常系のフローを GC タイミングに依存させる
- 例:ある処理が終わったかどうかを FinalizationRegistry だけで検知する
基本スタンスとして、「正しさは普通のコードで担保し、弱参照系はあくまで“メモリ最適化のオプション”」と考えるのが安全です。
コメント