⚠️正确使用 WeakXXX 对象需要仔细的考虑,最好尽量避免使用
- The JavaScript engine may hold references to things which look like they are unreachable (e.g., in closures, or inline caches).
- Different JavaScript engines may do these things differently, or the same engine may change its algorithms across versions.
WeakRef#
WeakRef 对象允许你保留对另一个对象的弱引用,而不会阻止被弱引用对象被 GC 回收
WeakMap && WeakSet#
一个对象作为 WeakMap 的键存在,不会阻止该对象被垃圾回收。一旦一个对象作为键被回收,那么在 WeakMap 中相应的值便成为了进行垃圾回收的候选对象,只要它们没有其他的引用存在。
可以这样理解:在 Map 中,value 是有因为 key 的存在而引用 + 1的;在 WeakMap 中,key 类似 WeakRef 的存在,不会引起 value 的引用 + 1;
WeakMap 有一个典型的使用场景:以DOM元素为key,指向的value存储一些额外的属性;如果该DOM元素被清除了,则 WeakMap 中的键值对会被自动GC,避免造成内存泄露;
参考 node --expose-gc[1]
WeakSet同理;
class IterableWeakMap {
<a class='tag' href="/tags/weakMap">#weakMap</a> = new WeakMap();
<a class='tag' href="/tags/refSet">#refSet</a> = new Set();
// FinalizationRegistry 存在的意义是当 key 被 GC 后,回调 cleanUp 清除 refSet 中的记录
// @DOC: new FinalizationRegistry((heldValue) => { /*...*/ })
// register(target, heldValue, ?unregisterToken)
// unregister(unregisterToken)
<a class='tag' href="/tags/finalizationGroup">#finalizationGroup</a> = new FinalizationRegistry(IterableWeakMap.#cleanup);
static #cleanup({ set, ref }) {
set.delete(ref);
}
constructor(iterable) {
for (const [key, value] of iterable) {
this.set(key, value);
}
}
set(key, value) {
const ref = new WeakRef(key);
this.<a class='tag' href="/tags/weakMap">#weakMap</a>.set(key, { value, ref });
this.<a class='tag' href="/tags/refSet">#refSet</a>.add(ref);
this.<a class='tag' href="/tags/finalizationGroup">#finalizationGroup</a>.register(key, {
set: this.<a class='tag' href="/tags/refSet">#refSet</a>,
ref
}, ref);
}
get(key) {
const entry = this.<a class='tag' href="/tags/weakMap">#weakMap</a>.get(key);
return entry && entry.value;
}
delete(key) {
const entry = this.<a class='tag' href="/tags/weakMap">#weakMap</a>.get(key);
if (!entry) {
return false;
}
this.<a class='tag' href="/tags/weakMap">#weakMap</a>.delete(key);
this.<a class='tag' href="/tags/refSet">#refSet</a>.delete(entry.ref);
this.<a class='tag' href="/tags/finalizationGroup">#finalizationGroup</a>.unregister(entry.ref);
return true;
}
*[Symbol.iterator]() {
for (const ref of this.#refSet) {
const key = ref.deref();
if (!key) continue;
const { value } = this.<a class='tag' href="/tags/weakMap">#weakMap</a>.get(key);
yield [key, value];
}
}
entries() {
return this[Symbol.iterator]();
}
*keys() {
for (const [key, value] of this) {
yield key;
}
}
*values() {
for (const [key, value] of this) {
yield value;
}
}
}
const key1 = { a: 1 };
const key2 = { b: 2 };
const keyValuePairs = [[key1, 'foo'], [key2, 'bar']];
const map = new IterableWeakMap(keyValuePairs);
for (const [key, value] of map) {
console.log(`key: ${JSON.stringify(key)}, value: ${value}`);
}
// key: {"a":1}, value: foo
// key: {"b":2}, value: bar
for (const key of map.keys()) {
console.log(`key: ${JSON.stringify(key)}`);
}
// key: {"a":1}
// key: {"b":2}
for (const value of map.values()) {
console.log(`value: ${value}`);
}
// value: foo
// value: bar
map.get(key1);
// → foo
map.delete(key1);
// → true
for (const key of map.keys()) {
console.log(`key: ${JSON.stringify(key)}`);
}
// key: {"b":2}
TLDR;#
javascript-memory-management[2]数据分配#
Stack: Static memory allocation#
- 编译期(compile time)
- 在栈(Stack)中
- 为每个值,包括 primitive values (strings, numbers, booleans, undefined, and null) and references
- 分配固定大小内存(fixed amount of memory )
Heap: Dynamic memory allocation#
- 在堆(Heap)
- 为 objects and functions 动态分配内存
- 注意:objects 中的 primitive values 仍然分配在 Stack
Garbage collection#
Reference-counting garbage collection#
- 当 no references 指向 objects 之后,就会被标记 GA
- 循环引用 | Cycles
- objects 出现了循环引用,即使将变量赋值为 null,也不能被 GA 标记
- 主要是此刻的 objects 仍在 heap 中持有相互的引用
Mark-and-sweep algorithm#
- 解决循环引用的问题
- 当变量不能被 root object 访问时候,就可以标记清除了
- root object:window(Browsers) / global(Node)
Memory leaks#
Global variables#
- 当变量不限定 scope 的时候(without var / let / const),将会自动 attach 到 root object;
- `use strict;` mode 下不允许上述赋值
- window.xxx = null;` 释放
Forgotten timers and callbacks#
- `clearInterval(intervalId);` 显式清除定时器
- `element.removeEventListener('click', onClick);` 显式清除监听事件
- `element.parentNode.removeChild(element);` 显式清除元素
Out of DOM reference#
const elements = [];
const element = document.getElementById('button');
elements.push(element);
function removeAllElements() {
elements.forEach((item, index) => {
document.body.removeChild(document.getElementById(item.id)); // 显式清除 DOM
elements.splice(index, 1); // 显式清除 Array 中的引用
});
}