[JavaScript] 内存管理

⚠️正确使用 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 中的引用
});
}

Date:
Words:
1078
Time to read:
5 mins