[JavaScript講座] 弱参照(WeakRef / FinalizationRegistry)

当ページのリンクには広告が含まれています。
目次

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 の注意点

  1. 正しさ(correctness)のために使ってはいけない
    • deref() で必ずオブジェクトが取れる前提」
      → この前提は成り立たない
    • 例:ユーザーデータを WeakRef にしか持たない → タイミングによって消える → バグ
  2. ロジック上「消えたら困る値」には使わない
    • ログイン中ユーザー・永続データ・UI状態などは弱参照ではなく普通の参照を使うべき
    • WeakRef はあくまで “あればラッキー”なキャッシュ 専用のイメージ
  3. 強参照がどこかに残っていれば、弱参照の意味はない 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 の制約・注意点

  1. 実行タイミングは一切保証されない
    • いつGCされるか → 不定
    • GCされたあと、いつコールバックが走るか → さらに不定
    • 極端な話、「プロセス終了まで一度も呼ばれない」こともある
  2. 実行されること自体が保証されない
    • プロセスが途中で終了すれば、当然コールバックは走らない
    • したがって、これを“後始末の唯一の手段”にしてはいけない
  3. ビジネスロジックに使ってはいけない
    • 例:DBからのレコード削除、ログ保存、ネットワーク通信など
    • それらは アプリケーション側で明示的に実行 すべきで、GC 任せにするのは危険
  4. パフォーマンス上のオーバーヘッドや複雑さ
    • 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 されたことを「あとから知る」仕組み

設計の順序としては:

  1. まず WeakMap / WeakSet で解決できないかを考える
  2. それでも足りず、かつ GC 依存の動作が本当に必要なときにWeakRef / FinalizationRegistry を検討する

くらいの慎重さでちょうど良いです。

いつ使うべきか?使うべきでないか?

使う“かもしれない”場面

  • かなり大規模なアプリ・ライブラリで
    • 「多数のオブジェクトに紐づくキャッシュ」
    • 「薄いラッパオブジェクト(ViewModel など)のライフサイクル管理」
  • パフォーマンスチューニング段階で、
    • 通常の strong ref / WeakMap だけではメモリが厳しいと判明し、「キャッシュをもっと積極的にGCに委ねたい」事情が生じたとき

ほとんどのアプリでやってはいけないこと

  • ビジネスロジックを WeakRef / FinalizationRegistry に依存させる
    • 例:ユーザー認証状態やセッションを弱参照だけで管理する
    • 例:GCに任せてDBレコードを削除する
  • 正常系のフローを GC タイミングに依存させる
    • 例:ある処理が終わったかどうかを FinalizationRegistry だけで検知する

基本スタンスとして、「正しさは普通のコードで担保し、弱参照系はあくまで“メモリ最適化のオプション”」と考えるのが安全です。


<<前へ(メタプログラミング)

>>次へ(コード品質とテスト)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次