JavaScript WeakRef与FinalizationRegistry的GC触发条件

351 阅读4分钟

WeakRefFinalizationRegistry 是 JavaScript 中用于与垃圾回收器(GC)协同工作的两个高级 API,它们不干预 GC 行为,但允许我们在 GC 发生之后获取“副作用”的钩子。本质上,它们的触发并非实时,而是受 GC 策略支配、时间不确定、不可预测的“异步响应机制”


结论:WeakRef 与 FinalizationRegistry 的 GC 触发条件简述

  • WeakRef 的“目标对象”只有在该对象“不可达”(Unreachable)时,才可能被 GC 回收。
  • FinalizationRegistry 的清理回调只会在目标对象被 GC 回收之后的某个未来时刻异步调用。
  • 两个 API 都不会强制触发 GC,也无法预测触发时机
  • 回收时机取决于宿主环境(如 V8)如何实现其 GC 策略:包括 GC 的时机、压力、内存紧张程度等。

WeakRef:对对象的“弱引用”

let obj = { name: "hello" };
let ref = new WeakRef(obj);

obj = null;

let derefValue = ref.deref();  // 有可能返回对象,也有可能已被GC回收后返回 undefined

GC 触发条件:

  • WeakRef 的目标对象 obj 必须处于不可达状态(Unreachable),即程序中不再有任何强引用。
  • GC 并不会立刻销毁目标对象,只有在垃圾回收算法下一轮扫描判定它可以清理时,才会清除它。
  • 即使 WeakRef 被创建,也不会“阻止” GC 清除该对象。

注意:

  • 使用 .deref() 获取对象后,如果此时 GC 没有清除它,你可以继续操作;
  • 你不能依赖 .deref() 返回始终有效对象,因为它可能随时失效。

FinalizationRegistry:对象死亡之后的清理通知

const registry = new FinalizationRegistry((heldValue) => {
  console.log('对象被回收:', heldValue);
});

let obj = { foo: "bar" };
registry.register(obj, "my-object");

obj = null;  // 此后某个时间,GC 会清理 obj,并异步调用注册函数

GC 触发条件:

  • 注册的对象必须在内存中彻底不可达,才可能被 GC 回收。
  • 回收后,JS 引擎会在“GC完成后的某个时刻”,调用注册回调(不是同步执行!)。
  • heldValue 不是目标对象,而是你设置的“替身值”,用于说明“哪个对象”被清除。

特别强调:

  • FinalizationRegistry 的回调不能保证执行时机
  • 不保证马上执行、不保证一定执行,甚至在低内存压力时完全不会被调用;
  • 它是一个“最佳努力”(best-effort)机制;
  • 不能用于核心业务逻辑,适合资源清理(如关闭连接、注销回调等);

实际中的 GC 触发机制(以 V8 为例)

V8 引擎(Chrome 和 Node.js 使用)中的 GC 策略具有多级触发机制:

  1. Minor GC(新生代 GC):对象分配压力大或空间满了;
  2. Major GC(老年代 GC):对象长期存活,内存紧张时触发;
  3. Idle GC(空闲时 GC):浏览器空闲帧期间补回收;
  4. Low memory GC:系统内存吃紧时强制触发。

WeakRef 和 FinalizationRegistry 都不影响 GC 是否执行,它们的工作依赖于:

  • 对象是否真的不可达;
  • 下一轮 GC 是否扫描到它;
  • 垃圾回收线程是否完成清理任务;
  • 是否处于允许调度注册回调的主线程状态。

为什么不能依赖 WeakRef 或 FinalizationRegistry 来做逻辑控制?

因为:

  • 它们不是 GC 的控制器,而是 GC 的旁观者
  • GC 本身不可预测,调度由宿主环境决定;
  • FinalizationRegistry 的回调可能永远不会被执行,例如程序运行短时间内未触发 GC;
  • 它们适合用于“非关键清理”:如释放缓存、移除监听器、日志记录等

冷知识与陷阱

❌ 错误用法:指望 WeakRef 判断对象是否“还活着”

const ref = new WeakRef(obj);
if (ref.deref()) {
  // 认为 obj 还活着?错!它可能刚好 GC 还没动手!
}

这不是“活着”的判断方法。它只是“此刻 GC 可能还没动手”。


✅ 正确用途示例:LRU 缓存弱引用条目

const cache = new Map();

function getData(id) {
  const ref = cache.get(id);
  const val = ref?.deref();

  if (val) return val;

  const newVal = loadData(id);
  cache.set(id, new WeakRef(newVal));
  return newVal;
}

当数据被垃圾回收,WeakRef 会自动无效,下一次访问时重新加载。


总结表:两者对比

特性WeakRefFinalizationRegistry
是否影响 GC否(不会阻止对象被 GC)否(对象仍会被正常 GC)
GC 触发条件目标对象无强引用且被 GC 回收同上
是否保证触发不保证不保证
回调是否同步无回调异步回调
应用场景缓存、软引用清理副资源、日志、释放钩子等
是否能判断对象“还活着”

结语

WeakRef 与 FinalizationRegistry 是为应对现代复杂内存生命周期而生的工具。它们并不是让你掌控垃圾回收,而是允许你“在对象死亡之后做点事”。

使用时需牢记两个原则:

  1. 它们无法预测、控制或强制 GC
  2. 它们只是 GC 行为之后的观察器,适合辅助优化而非核心逻辑