代码之家  ›  专栏  ›  技术社区  ›  jfriend00

为什么在for循环中声明的变量的最后一次迭代没有得到垃圾回收?

  •  0
  • jfriend00  · 技术社区  · 5 年前

    我的问题是这是否是一个nodejs垃圾收集器错误?或者这是意料之中的事?

    在Windows上运行node v14.15.0。

    在为你的 this question 在涉及WeakRef对象时,我发现了一个关于垃圾收集的奇怪的东西,它似乎是一个可能的bug。分配给在一个变量中声明的变量的对象 for let 变量超出了 对于 循环。这里感兴趣的变量命名为 element 这是它的循环。只是循环最后一次迭代的对象没有得到GCed(即 要素 最后指向):

    // fill all the arrays and the cache
    // and put everything into the holding array too
    for (let i = 0; i < numElements; i++) {
        let arr = new Array(lenArrays);
        arr.fill(i);
        let 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));
    }
    

    holding 有了这个:

    holding.length = 0;
    

    所有的值 要素 从那个循环中应该可以得到GC。唯一提到他们的是via WeakRef 对象(不阻止GC)。

    事实上,如果我让nodejs有一些空闲时间,那么除了由 对于 循环确实是GCed。但是,最后一个不是。如果我加上 element = null 到最后 对于 循环,最后一个就可以拿到GCed了。所以,nodejs没有清除 要素 要素

    因此,您可以在这里看到整个代码(您可以将其放到一个文件中,自己在nodejs中运行):

    'use strict';
    
    // 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
        for (let i = 0; i < numElements; i++) {
            let arr = new Array(lenArrays);
            arr.fill(i);
            let 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));
        }
    
        // 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 been 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: 805,544,472
    heapUsed: 805,582,072
    undefined
    undefined
    {
      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
      ]
    }
    Index item 9999 still in cache
    There are 1 items that haven't been GCed in the map
    heapUsed: 3,490,168
    

    两个 undefined 需要行。id:9999对象的第二个记录输出不应出现。也应该是 . 而且,不希望找到仍在缓存中的id:9999对象。它应该是合格的GC。


    一种可能的理论是V8优化器正在 要素 对于 循环,以避免在循环中一次又一次地创建它,但在循环完成后又不能使它符合GC的条件—实际上是将它提升到更高的范围。

    另一种理论是GC并不总是块作用域粒度。

    有没有虫子?

    0 回复  |  直到 5 年前
        1
  •  2
  •   jmrk    5 年前

    MDN documentation 他说:

    同样重要的是,要避免依赖于规范中没有保证的任何特定行为。何时、如何以及是否进行垃圾回收取决于任何给定JavaScript引擎的实现。

    element 在循环后超出范围就JavaScript语言语义而言,不能保证/承诺/规范 let

    如果释放不可访问对象的失败导致内存无限增长,直到OOM崩溃发生,那么就是一个bug。但事实并非如此,而是 numElements 到1或10或10000。

    global.gc() 很好,您还需要返回到事件循环,以便看到WeakRefs被清除(正如MDN文档也指出的那样)。


    编辑以添加:
    在这种特殊情况下 要素 留下来是因为 代码/字节码只是为每个局部变量分配一个堆栈槽。它不需要在函数返回之前清空该插槽,因此堆栈插槽引用的对象将保持活动状态,直到函数返回。这通常(没有WeakRefs)是不可观察的,并且只是执行速度、启动延迟、内存消耗、CPU/功耗、代码复杂性和/或引擎所做的其他度量之间的许多权衡之一。这些内部细节是有意不记录的,因为它们可以随时更改,任何人都不应该依赖它们(正如MDN文档指出的那样)。
    如果你强迫这个函数 run
    也就是说,虽然我理解你的好奇心,但我想再次强调:内部细节其实并不重要。JS引擎中到底发生了什么在很大程度上取决于整个场景,当然也取决于您运行的引擎和它的版本。

    推荐文章