javascript - 为什么 for 循环中声明的变量的最后一次迭代没有被垃圾收集?

标签 javascript node.js garbage-collection v8 weak-references

我的问题是这是否是一个nodejs垃圾收集器错误?或者这是某种预期的结果?

在 Windows 上运行 Node v14.15.0。

在寻找 this question 的答案时涉及 WeakRef 对象时,我发现了关于垃圾收集的一个奇怪的事情,这似乎是一个可能的错误。分配给 for 中声明的变量的对象即使在那之后循环也没有被垃圾收集let变量超出 for 的范围环形。这里感兴趣的变量被命名为 element这是它所在的循环。它只是循环的最后一次迭代中没有被 GC 的对象(element 最后指向的那个对象):

// 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;

你可能会认为在这个循环完成之后和holding之后已被清除,element的所有值从该循环应该有资格进行 GC。对它们的唯一引用是通过 WeakRef对象(不会阻止 GC)。

事实上,如果我让nodejs有一些空闲时间,除了for创建的最后一个对象之外的所有对象都会被删除。循环确实是GCed。但是,最后一个不是。如果我添加 element = nullfor结束循环,然后最后一个就被 GC 了。因此,不知何故,nodejs 没有清除 element 变量上的 refcnt最后指出,尽管 element现在超出​​了范围。

所以,您可以在这里看到完整的代码(您可以将其放入文件中并自己在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 对象的第二个记录输出不是预期的。它也应该是undefined 。并且,预计不会在缓存中找到 id:9999 对象。它应该有资格获得 GC。


一种可能的理论是 V8 优化器正在拉 elementfor循环以避免必须在循环内一遍又一遍地创建它,但在循环完成后不使其符合 GC 的条件 - 实质上是将其提升到更高的范围。

另一个理论是 GC 并不总是 block 范围粒度。

是否有错误?

最佳答案

这不是一个错误。我同意这里的行为乍一看很奇怪,但正如MDN documentation说:

It's also important to avoid relying on any specific behaviors not guaranteed by the specification. When, how, and whether garbage collection occurs is down to the implementation of any given JavaScript engine.

虽然就 JavaScript 语言语义而言,element 在循环之后确实超出了范围,但不能保证/ promise /规范 的对象>let-指向的循环(或其他 block )中的变量可以在该 block 末尾立即进行垃圾回收。发动机可以自由地用于例如在内部为此变量分配一个堆栈槽,该堆栈槽只会在当前函数结束时被清除;堆栈槽通常被 GC 视为“根”,即它们使它们指向的内容保持事件状态。

如果无法释放无法访问的对象导致内存无限增长,直到发生 OOM 崩溃,那么这将是一个错误。但这里的情况并非如此:无论您将 numElements 设置为 1、10 还是 10000,它都是一个 对象,一直保留到函数结束。

旁注:无需休眠五秒钟即可运行 GC; Node 的 global.gc() 就可以了,您只需要返回到事件循环即可看到 WeakRefs 被清除(正如 MDN 文档也指出的那样)。


编辑添加:
在这种特殊情况下,最后一个元素保留下来的具体原因是因为未优化的代码/字节码只是为每个局部变量分配了一个堆栈槽。它不会在函数返回之前将该槽清空,因此堆栈槽引用的对象将保持事件状态,直到函数返回。这通常(没有 WeakRefs)是不可观察的,并且只是执行速度、启动延迟、内存消耗、CPU/功耗、代码复杂性和/或引擎所做的其他指标之间的众多权衡之一。这些内部细节故意没有记录下来,因为它们可以随时更改,任何人都不应该依赖它们(正如 MDN 文档指出的那样)。
如果您强制函数 run 一段时间后进行优化,优化编译器将花费时间进行适当的生存范围分析,这通常会导致堆栈槽在函数执行时被重用于不同的事情进展,并且(至少在这种情况下)导致该对象实际上会更快地被垃圾收集。
也就是说,虽然我理解您的好奇心,但我想再次强调:内部细节确实并不重要。 JS 引擎内部究竟发生了什么很大程度上取决于整体场景,当然,具体情况也会根据您运行的引擎及其版本而变化。

关于javascript - 为什么 for 循环中声明的变量的最后一次迭代没有被垃圾收集?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67646525/

相关文章:

javascript - NodeJS,多次尝试获取两点之间的 360 度 Angular 失败

java - 了解java中的垃圾收集

Java GC 没有第二次收集 "zombie"对象

javascript - javascript中函数的异步执行

javascript - 如何根据Js数组中保存的Id获取分散在数组字段中的Object

javascript - 使用 Layout, Partials with Handlebars 模板

node.js - 仅登录 1 个谷歌帐户时,Passport Google Oauth2 不提示选择帐户

node.js - Mongoose - 按标准查找子文档

Javascript,用Promises拼接FileReader处理大文件,怎么样?

java - PDFBOX java.lang.OutOfMemoryError : java heap space; GC overhead limit exceeded 错误