首页 > 解决方案 > 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) 成本)?。

标签: javascriptnode.jslookupweak-referenceslookup-tables

解决方案


这里真正的问题是调用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 异常的问题。


推荐阅读