Skip to content

Latest commit

 

History

History
148 lines (117 loc) · 4.74 KB

v8提供的一个内存高级玩法.md

File metadata and controls

148 lines (117 loc) · 4.74 KB
date tags
2020-11-28
javascript

WeakMap提供了弱引用,不过毕竟仅仅局限在Map,后来又有了WeakRef,这里说的很详细,基于官方给的例子,仅仅总结下重点:

  • 可对任意变量弱引用(WeakRef)
  • 加入了垃圾回收回调(FinalizationRegistry)

解决什么问题?降低遗漏手动回收导致内存泄露的风险。问题简述:

class MovingAvg {
  constructor(socket) {
    this.events = [];
    this.socket = socket;
    this.listener = (ev) => { this.events.push(ev); };
    socket.addEventListener('message', this.listener);
  }

  compute(n) {
    // Compute the simple moving average for the last n events.
    // …
  }
}

上面的socket通过一个closureref了MovingAvg实例,可能对于很多js新手,这是一个比较隐蔽的ref。然后下面的这个类使用了这个类:

class MovingAvgComponent {
  constructor(socket) {
    this.socket = socket;
  }

  start() {
    this.movingAvg = new MovingAvg(this.socket);
  }

  stop() {
    // Allow the garbage collector to reclaim memory.
    this.movingAvg = null;
  }

  render() {
    // Do rendering.
    // …
  }
}

stop看似清理了MovingAvg,实际上没有,socket还通过保持一个closure引用了当前的MovingAvg实例。那么加个dispose吧:

class MovingAvgComponent {
  constructor(socket) {
    this.socket = socket;
  }

  start() {
    this.movingAvg = new MovingAvg(this.socket);
  }

  stop() {
    // Allow the garbage collector to reclaim memory.
    this.movingAvg.dispose()
    this.movingAvg = null;
  }

  render() {
    // Do rendering.
    // …
  }
}

class MovingAvg {
  constructor(socket) {
    this.events = [];
    this.socket = socket;
    this.listener = (ev) => { this.events.push(ev); };
    socket.addEventListener('message', this.listener);
  }

  dispose() {
    this.socket.removeEventListener('message', this.listener);
  }

  // …
}

那么问题来了,清理MovingAvg实例需要两步走:

  1. this.movingAvg.dispose()
  2. this.movingAvg = null

所以清理步骤的风险就变大了。或许对于某些人来说不能算是问题,而且毕竟,即使清理只需要一步,还是会有人犯错。但毕竟v8是搞底层的了,解决机制性问题就是它的工作。WeakRef来解决:

function addWeakListener(socket, listener) {
  const weakRef = new WeakRef(listener);
  const wrapper = (ev) => { weakRef.deref()?.(ev); };
  socket.addEventListener('message', wrapper);
}

class MovingAvg {
  constructor(socket) {
    this.events = [];
    this.listener = (ev) => { this.events.push(ev); };
    addWeakListener(socket, this.listener);
  }
}

const weakRef = new WeakRef(listener);建立了弱ref关系,如果listener被回收了,ref关系就解除了weakRef.deref()就返回undefined。不过wrapper没有用了,它也需要被回收。FinalizationRegistry来解决,整个一套长这样的:

const gListenersRegistry = new FinalizationRegistry(({ socket, wrapper }) => {
  socket.removeEventListener('message', wrapper); // 6
});

function addWeakListener(socket, listener) {
  const weakRef = new WeakRef(listener); // 2
  const wrapper = (ev) => { weakRef.deref()?.(ev); }; // 3
  gListenersRegistry.register(listener, { socket, wrapper }); // 4
  socket.addEventListener('message', wrapper); // 5
}

class MovingAvg {
  constructor(socket) {
    this.events = [];
    this.listener = (ev) => { this.events.push(ev); }; // 1
    addWeakListener(socket, this.listener);
  }
}

整个过程,还是建议看看v8的原文解释👇:

We make an event listener and assign it to this.listener so that it is strongly referenced by the MovingAvg instance (1). We then wrap the event listener that does the work in a WeakRef to make it garbage-collectible, and to not leak its reference to the MovingAvg instance via this (2). We make a wrapper that deref the WeakRef to check if it is still alive, then call it if so (3). We register the inner listener on the FinalizationRegistry, passing a holding value { socket, wrapper } to the registration (4). We then add the returned wrapper as an event listener on socket (5). Sometime after the MovingAvg instance and the inner listener are garbage-collected, the finalizer may run, with the holding value passed to it. Inside the finalizer, we remove the wrapper as well, making all memory associated with the use of a MovingAvg instance garbage-collectible (6).

注意事项:

  • FinalizationRegistry
    • 它得是个全局变量,它得负责回收别人,自己别被回收了
    • 回调执行的时机顺序都不确定,不要放重要逻辑在里面
  • 防止一些微妙的closure ref,addWeakListener放在constructor的外面