当前位置: 首页 > 面试题库 >

为什么尝试写入大文件会导致js堆内存不足

丌官远
2023-03-14
问题内容

此代码

const file = require("fs").createWriteStream("./test.dat");
for(var i = 0; i < 1e7; i++){

    file.write("a");
}

运行约30秒后给出此错误消息

<--- Last few GCs --->

[47234:0x103001400]    27539 ms: Mark-sweep 1406.1 (1458.4) -> 1406.1 (1458.4) MB, 2641.4 / 0.0 ms  allocation failure GC in old space requested
[47234:0x103001400]    29526 ms: Mark-sweep 1406.1 (1458.4) -> 1406.1 (1438.9) MB, 1986.8 / 0.0 ms  last resort GC in old spacerequested
[47234:0x103001400]    32154 ms: Mark-sweep 1406.1 (1438.9) -> 1406.1 (1438.9) MB, 2628.3 / 0.0 ms  last resort GC in old spacerequested


<--- JS stacktrace --->

==== JS stack trace =========================================

Security context: 0x30f4a8e25ee1 <JSObject>
    1: /* anonymous */ [/Users/matthewschupack/dev/streamTests/1/write.js:~1] [pc=0x270efe213894](this=0x30f4e07ed2f1 <Object map = 0x30f4ede823b9>,exports=0x30f4e07ed2f1 <Object map = 0x30f4ede823b9>,require=0x30f4e07ed2a9 <JSFunction require (sfi = 0x30f493b410f1)>,module=0x30f4e07ed221 <Module map = 0x30f4edec1601>,__filename=0x30f493b47221 <String[49]: /Users/matthewschupack/dev/streamTests/...

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
 1: node::Abort() [/usr/local/bin/node]
 2: node::FatalException(v8::Isolate*, v8::Local<v8::Value>, v8::Local<v8::Message>) [/usr/local/bin/node]
 3: v8::internal::V8::FatalProcessOutOfMemory(char const*, bool) [/usr/local/bin/node]
 4: v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationSpace) [/usr/local/bin/node]
 5: v8::internal::Runtime_AllocateInTargetSpace(int, v8::internal::Object**, v8::internal::Isolate*) [/usr/local/bin/node]
 6: 0x270efe08463d
 7: 0x270efe213894
 8: 0x270efe174048
[1]    47234 abort      node write.js

而这段代码

const file = require("fs").createWriteStream("./test.dat");
for(var i = 0; i < 1e6; i++){

    file.write("aaaaaaaaaa");//ten a's
}

几乎可以立即完美运行,并产生10MB的文件。据我了解,流的要点是两个版本应该在大约相同的时间内运行,因为数据是相同的。即使a每次迭代将s
的数量增加到100或1000,也几乎不会增加运行时间,并且写入1GB文件没有任何问题。在1e6次迭代中每次迭代编写一个字符也可以正常工作。

这里发生了什么?


问题答案:

发生内存不足错误是因为您没有等待drain事件的发出,而没有等待Node.js将缓冲所有写入的块,直到出现最大的内存使用量为止。

.write``false如果内部缓冲区大于highWaterMark默认值16384字节(16kb),将返回。在您的代码中,您没有处理的返回值.write,因此永远不会刷新缓冲区。

使用以下命令可以很容易地测试它: tail -f test.dat

执行脚本时,您会看到test.dat直到脚本完成为止都没有任何内容在写。

对于1e7缓冲区应清除610次。

1e7 / 16384 = 610

一种解决方案是检查.write返回值,如果false返回,请使用file.once('drain')包装在promise中的包,直到drain事件被发出为止。

注意: writable.writableHighWaterMark已在节点v9.3.0中添加

const file = require("fs").createWriteStream("./test.dat");

(async() => {

    for(let i = 0; i < 1e7; i++) {
        if(!file.write('a')) {
            // Will pause every 16384 iterations until `drain` is emitted
            await new Promise(resolve => file.once('drain', resolve));
        }
    }
})();

现在,如果您这样做,tail -f test.dat您将看到脚本仍在运行时如何写入数据。

关于为什么出现1e7而不是1e6的内存问题的原因,我们必须研究一下Node.Js如何进行缓冲,这发生在writeOrBuffer函数中。

此示例代码将使我们对内存使用情况有一个大概的估计:

const count = Number(process.argv[2]) || 1e6;
const state = {};

function nop() {}

const buffer = (data) => {
    const last = state.lastBufferedRequest;
    state.lastBufferedRequest = {
      chunk: Buffer.from(data),
      encoding: 'buffer',
      isBuf: true,
      callback: nop,
      next: null
    };

    if(last)
      last.next = state.lastBufferedRequest;
    else
      state.bufferedRequest = state.lastBufferedRequest;

    state.bufferedRequestCount += 1;
}

const start = process.memoryUsage().heapUsed;
for(let i = 0; i < count; i++) {
    buffer('a');
}
const used = (process.memoryUsage().heapUsed - start) / 1024 / 1024;
console.log(`${Math.round(used * 100) / 100} MB`);

执行时:

// node memory.js <count>
1e4: 1.98 MB
1e5: 16.75 MB
1e6: 160 MB
5e6: 801.74 MB
8e6: 1282.22 MB
9e6: 1442.22 MB - Out of memory
1e7: 1602.97 MB - Out of memory

因此,每个对象都使用~0.16 kb,并且在writes不等待drain事件的情况下执行1e7时,您的内存中就有1000万个对象(公平地说,它在达到10M之前就崩溃了)

不管使用单个a还是1000,存储的增加都可以忽略不计。

您可以使用--max_old_space_size={MB}flag 来增加节点使用的最大内存
(当然这不是解决方案,仅用于检查内存消耗而不使脚本崩溃)

node --max_old_space_size=4096 memory.js 1e7

更新 :我在内存片段上犯了一个错误,导致内存使用量增加了30%。我正在为每个.writeNode重用nop回调创建一个新的回调。

更新二

如果您始终写入相同的值(在实际情况下是可疑的),则可以通过每次传递相同的缓冲区来 大大 减少内存使用和执行时间:

const buf = Buffer.from('a');
for(let i = 0; i < 1e7; i++) {
    if(!file.write(buf)) {
        // Will pause every 16384 iterations until `drain` is emitted
        await new Promise(resolve => file.once('drain', resolve));
    }
}


 类似资料:
  • 我正在玩rxjava,发现如果在活动被销毁之前没有完成订阅,则存在内存泄漏的风险,因为“可观察对象保留对上下文的引用”。如果订阅没有取消订阅,则此类情况的演示之一如下所示。已销毁(来源:https://github.com/dlew/android-subscription-leaks/blob/master/app/src/main/java/net/danlew/rxsubscriptions

  • 问题内容: 当我使用以下代码读取文件时: 我有以下错误 文件大小是 问题答案: 显然,文件太大,无法一次全部读入内存。 为什么不使用: 或者,如果您未使用Python 2.6和更高版本,则: 在这两种情况下,您都将获得一个迭代器,该迭代器可以像对待字符串列表一样对待。 编辑:由于您将整个文件读取为一个大字符串然后在换行符上进行拆分的方式将删除过程中的换行符,因此我在示例中添加了a ,以便更好地模拟

  • 我们在日志中看到了OutOfMemoryExceptions,它们似乎与java堆提交大小从~1G增长到~2.4G一致。尽管有错误消息,但堆空间似乎没有用完。除了抛出异常(和生成的堆转储)之外,调整大小似乎最终会成功,应用程序继续运行,没有任何问题(堆提交大小约2.4G)。 下面是日志输出的一个示例: 请注意,在OOME之前,提交的堆总数在1GB和2.4GB之间振荡。我们可以看到,它之前非常稳定在

  • 问题内容: 这是代码片段。 编辑:我正在从目录中读取文件。因此,我需要在每个循环中打开阅读器。我进行了一些修改,但同时也没有写入该文件。这是代码: 编辑:我修改了代码,但没有成功, 而且我收到此错误: 编辑:谢谢..我想通了。实际上,我在eclipse中创建了一个目录,但没有刷新它来查看内容。真是太傻了…还是非常感谢 问题答案: 您正在循环内创建FileWritter,因此您将始终在每个循环中截断

  • 问题内容: 遇到一个错误地使用 而不是 在其代码中的人,它没有显示为编译错误。 是因为 是相同的 ? 问题答案: 没有编译错误,因为它是有效的(尽管相当无用) 一元运算符 ,其使用方式与以下方式相同: Java语言规范中的相关部分是Unary Plus运算符+(第15.15.3节) 。它指定调用一元运算会导致操作数的一元数值提升(第5.6.1节)。这意味着: * 如果操作数是编译时类型的,,,或,

  • 问题内容: 好吧,我试图理解并阅读可能导致它的原因,但我却无法理解: 我的代码中有这个地方: 事实是,当它尝试调用某些方法时,它将引发而不是其他预期的异常(特别是)抛出 。我实际上知道调用了什么方法,所以我直接转到该方法代码,并为应该抛出的行添加了一个块 ,它实际上按预期抛出。然而,当它上升时,以某种方式更改了上面的代码并没有 按预期进行。 是什么原因导致这种行为的?我该如何检查? 问题答案: 通