【问题标题】:Lookup table with weak references in JavascriptJavascript中弱引用的查找表
【发布时间】:2021-05-21 21:38:58
【问题描述】:

我有一个动态添加和删除元素的树结构。元素是从网络动态加载的。我想要实现的是有一个查找表,将元素的 id 映射到树中的实际元素。现在,使用简单的 Map 或 Object 的问题是它持有对树元素的强引用,这会在一段时间后使内存膨胀。由于 node >= 14.6.0 和 Chrome >= 84 supposedly support 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());

正如上面评论中所写,问题是循环永远不会结束 因为 deref 调用总是返回一个值,尽管元素 var 设置为 null。

我在这里遗漏了什么吗?如果没有,这就是它应该如何工作, 我怎样才能实现拥有弱引用映射的目标(WeakMap 不是 这里有一个选项,因为我会花费 O(n) 来通过 id 查找元素)?。

【问题讨论】:

  • 我从来没有这样做过,但也许我应该有。你能多说一下这个的用例吗?为什么不能让 map 超出范围,同时元素本身也超出我们的范围,在这种情况下,GC 行为将是相同的。
  • 地图本身永远不会超出范围。它在一个单例中,保存对所有活动树元素的引用(或对从服务中获取的一些动态元素的引用,其中获取它的成本将超过查找和重用)。至少这是应该的。
  • 如果你熟悉mobx-state-tree:它类似于resolveIdentifier。基本上用 O(1) 而不是 O(n) 查找树中的元素(树不是二元的,不是排序的)。 resolveIdenfier 和他们的查找图的事情是:他们知道对象何时死亡,如果从查找图中删除。我以为我可以更“懒惰”地处理它。
  • 所以它是一个缓存,但是您不能手动从缓存中删除元素,因为有时这超出了您的控制范围?是这个问题吗?
  • 不,你不能。请阅读 WeakMap 上的文档。 WeakMap 只接受 objects 作为键,而不接受原始类型。在@jfriend00 对您的回答的回复中也提到了。

标签: javascript node.js lookup weak-references lookup-tables


【解决方案1】:

我错过了什么吗?

是的:您缺少链接到的 documentation 中的注释,例如:

如果您的代码刚刚为目标对象创建了 WeakRef,或者从 WeakRef 的 deref 方法中获取了目标对象,则该目标对象将在当前 JavaScript 作业(包括任何承诺反应作业在脚本作业结束时运行)。也就是说,您只能“看到”一个对象在事件循环的轮次之间被回收。

当然还有:

尽可能避免
正确使用 Wea​​kRef 需要仔细考虑,如果可能的话最好避免。避免依赖规范未保证的任何特定行为也很重要。何时、如何以及是否发生垃圾收集取决于任何给定 JavaScript 引擎的实现。

也就是说,实现你的目标是完全可能的;你的测试用例太简单了(根据上面引用的注释)来展示它。这是一个固定版本:

const lookup = new Map();

(function () {
  let element = { id: "someid", data: {} };
  lookup.set(element.id, new WeakRef(element));
  element = null;

  console.log(lookup.get("someid").deref());

  setTimeout(() => {
    global.gc();
    console.log(lookup.get("someid").deref());
  }, 0);
})();

【讨论】:

    【解决方案2】:

    这里真正的问题是调用global.gc() 不会运行完整的GC pass。在我下面的测试中,只有当我允许 10 秒的空闲时间时,我才能获得完整的 GC。

    以下是对您的特定代码的一些观察。如果我在 GC 中添加一个 await delay(5000) 暂停,那么您的对象在 while 循环之前仍然没有被 GC。但是,如果我添加两个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。所以,我建议你插入一个合法的暂停(就像我在我的例子中所做的那样),看看你是否最终能恢复记忆。


    附:我发布了this other question 关于我在处理此答案时发现的 GC 异常。

    【讨论】:

    • global.gc 确实运行了一个完整的 GC 周期。您不需要任何一秒钟的延迟。但是,要看到 WeakRefs 被清除,您确实需要转动事件循环(这是中断异步函数的副作用,就像您的 delay 所做的那样)。有关示例,请参阅 MDN 文档或我的答案。
    • 非常感谢您的努力。但是,我已经接受了@jmrk 的回答,因为似乎我测试所需的只是 setTimeout(..., 0) 并且它工作得很好,这意味着整个 gc 周期确实不需要更长的等待。
    【解决方案3】:

    我认为您的代码运行良好(当然,除了 while 循环和 global.gc())。如果你在 Chrome 中运行这个测试页面,然后等待一段时间,最终它会在控制台中记录许多 WeakRefs 确实已被垃圾收集:https://output.jsbin.com/momelej

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多