javascript - Javascript中弱引用的查找表
问题描述
我有一个动态添加和删除元素的树结构。元素是从网络动态加载的。我想要实现的是有一个查找表,将元素的 id 映射到树中的实际元素。现在,使用简单的 Map 或 Object 的问题是它持有对树元素的强引用,这会在一段时间后使内存膨胀。由于 node >= 14.6.0 和 Chrome >= 84应该支持 WeakRef's我想我可以制作一个 Map 将 WeakRefs 保存到我的树元素,然后简单地deref()
查看元素是否仍然存在。我试图对此进行测试,但它似乎不起作用。我的最小测试如下所示:
const lookup = new Map();
let element = new Object({id:"someid", data: {}});
lookup.set(element.id, new WeakRef(element));
console.dir(lookup.get("someid").deref());
// as expected output is { id: 'someid', data: {} }
element = null;
console.log(element);
// as expected output is null
// simply calling global.gc() didn't work
// so i made this loop which allocates mem *and* calls global.gc() to
// force garbage collection
// problem: infinite loop because deref always returns the dereferenced
// value which should have gone since element was set to null
while (lookup.get("someid").deref()) {
const a = new Array(1000);
// enabled with --expose-gc in node
global.gc();
}
console.dir(lookup.get("someid").deref());
正如上面在评论中所写,问题是循环永远不会结束,因为尽管元素 var 被设置为 null,但 deref 调用总是返回一个值。
我在这里错过了什么吗?如果不是,这就是它应该如何工作,我怎样才能实现拥有弱引用映射的目标(WeakMap 在这里不是一个选项,因为通过 id 查找元素需要 O(n) 成本)?。
解决方案
这里真正的问题是调用global.gc()
不会运行完整的 GC 传递。在下面的测试中,只有当我允许 10 秒的空闲时间时,我才能获得完整的 GC。
以下是有关您的特定代码的一些观察结果。如果我在 GC 中添加一个await delay(5000)
暂停,那么您的对象在循环之前仍然没有被 GC while
。但是,如果我添加两条await delay(5000)
语句或一条await delay(10000)
语句,它会在while
循环之前进行 GC。因此,GC 显然是时间敏感的,并且调用global.gc()
显然不是 GC 的完整运行。例如,这是您的代码的一个版本,其中 weakref 被 GCed!
function delay(t, v) {
return new Promise(resolve => {
setTimeout(resolve, t, v);
});
}
async function run() {
const lookup = new Map();
let element = new Object({ id: "someid", data: {} });
lookup.set(element.id, new WeakRef(element));
console.dir(lookup.get("someid").deref());
// as expected output is { id: 'someid', data: {} }
element = null;
await delay(10000);
console.log(element);
// as expected output is null
// if above is delay(5000), then it logs "in while loop"
// if above is delay(10000), then it does NOT log "in while loop"
// so the amount of time is important to allow the GC to do its thing
while (lookup.get("someid").deref()) {
console.log("in while loop");
break;
}
console.dir(lookup.get("someid").deref());
}
run();
在我发现您的代码会延迟 GC 之前,我开始进行实验以查看 WeakRef 是否有效。这是显示的代码(具有允许完整 GC 的正确延迟),WeakRef 确实在节点 v14.15 中工作。
这是我的测试代码:
// to make memory usage output easier to read
function addCommas(str) {
var parts = (str + "").split("."),
main = parts[0],
len = main.length,
output = "",
i = len - 1;
while (i >= 0) {
output = main.charAt(i) + output;
if ((len - i) % 3 === 0 && i > 0) {
output = "," + output;
}
--i;
}
// put decimal part back
if (parts.length > 1) {
output += "." + parts[1];
}
return output;
}
function delay(t, v) {
return new Promise(resolve => {
setTimeout(resolve, t, v);
});
}
function logUsage() {
let usage = process.memoryUsage();
console.log(`heapUsed: ${addCommas(usage.heapUsed)}`);
}
const numElements = 10000;
const lenArrays = 10000;
async function run() {
const cache = new Map();
const holding = [];
function checkItem(n) {
let item = cache.get(n).deref();
console.log(item);
}
// fill all the arrays and the cache
// and put everything into the holding array too
let arr, element;
for (let i = 0; i < numElements; i++) {
arr = new Array(lenArrays);
arr.fill(i);
element = { id: i, data: arr };
// temporarily hold onto each element by putting a
// full reference (not a weakRef) into an array
holding.push(element);
// add a weakRef to the Map
cache.set(i, new WeakRef(element));
}
// clean up locals we don't need any more
element = array = null;
// should have a big Map holding lots of data
// all items should still be available
checkItem(numElements - 1);
logUsage();
await delay(5000);
logUsage();
// make whole holding array contents eligible for GC
holding.length = 0;
// pause for GC, then see if items are available
// and what memory usage is
await delay(5000);
checkItem(0);
checkItem(1);
checkItem(numElements - 1);
// count how many items are still in the Map
let cnt = 0;
for (const [index, item] of cache) {
if (item.deref()) {
++cnt;
console.log(`Index item ${index} still in cache`);
}
}
console.log(`There are ${cnt} items that haven't be GCed in the map`);
logUsage();
}
run();
而且,我得到的输出是这样的:
{
id: 9999,
data: [
9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999,
9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999,
9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999,
9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999,
9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999,
9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999,
9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999,
9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999,
9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999,
9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999, 9999,
... 9900 more items
]
}
heapUsed: 806,706,120
heapUsed: 806,679,456
undefined
undefined
undefined
There are 0 items that haven't be GCed in the map
heapUsed: 3,412,144
输出中的两undefined
行和最后一行 heapUsed 表明,包装在 weakRef 引用中的对象确实被 GC 了。
因此,经过足够长的时间延迟,解释器无事可做,只有 weakRef 的数据似乎被 GCed。我还不知道为什么您的示例没有显示这一点,除非我的经验表明,仅调用global.gc()
并不一定会执行与实际空闲解释器将执行的所有相同的 GC。所以,我建议你插入一个合法的暂停(就像我在我的例子中所做的那样),看看你是否最终会恢复记忆。
PS 我发布了另一个关于我在处理这个答案时发现的 GC 异常的问题。