为什么v8在这种情况下内存不足?

根据node.js文档,一个节点在32位版本上有一个512meg的限制,在64bit版本上有一个1.4gig的限制。 Chrome AFAICT的限制是相似的。 (+/- 25%)

那么,为什么这个代码在内存使用量不超过424meg的时候会耗尽内存呢?

这里是代码( 代码是无稽之谈,这个问题不是关于代码是做什么的,这是关于代码失败的原因 )。

var lookup = 'superCaliFragilisticExpialidosiousThispartdoesnotrealllymattersd'; function encode (num) { return lookup[num]; } function makeString(uint8) { var output = ''; for (var i = 0, length = uint8.length; i < length; i += 3) { var temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2]); output += encode(temp >> 18 & 0x3F) + encode(temp >> 12 & 0x3F) + encode(temp >> 6 & 0x3F) + encode(temp & 0x3F); } return output; } function test() { var big = new Uint8Array(64 * 1024 * 1024 + 2); // multiple of 3 var str = makeString(big); console.log("big:", big.length); console.log("str:", str.length); } test(); 

正如你所看到的, makeString通过一次追加4个字符makeString构build一个string。 在这种情况下,它会build立一个长度(180meg)大的string89478988。 由于output被附加到,最后一次附加字符将有2个string在内存中。 旧的89478984字符和最后一个89478988.GC应收集任何其他使用的内存。

所以,64meg(原始数组)+ 180meg * 2 = 424meg。 那么在V8的限制下。

但是,如果运行该示例,则会失败,内存不足

 <--- Last few GCs ---> 3992 ms: Scavenge 1397.9 (1458.1) -> 1397.9 (1458.1) MB, 0.2 / 0 ms (+ 1.5 ms in 1 steps since last GC) [allocation failure] [incremental marking delaying mark-sweep]. 4450 ms: Mark-sweep 1397.9 (1458.1) -> 1397.9 (1458.1) MB, 458.0 / 0 ms (+ 2.9 ms in 2 steps since start of marking, biggest step 1.5 ms) [last resort gc]. 4909 ms: Mark-sweep 1397.9 (1458.1) -> 1397.9 (1458.1) MB, 458.7 / 0 ms [last resort gc]. $ node foo.js <--- JS stacktrace ---> ==== JS stack trace ========================================= Security context: 0x3a8521e3ac1 <JS Object> 2: makeString(aka makeString) [/Users/gregg/src/foo.js:~6] [pc=0x1f83baf53a3b] (this=0x3a852104189 <undefined>,uint8=0x2ce813b51709 <an Uint8Array with map 0x32f492c0a039>) 3: test(aka test) [/Users/gregg/src/foo.js:19] [pc=0x1f83baf4df7a] (this=0x3a852104189 <undefined>) 4: /* anonymous */ [/Users/gregg/src/foo.js:24] [pc=0x1f83baf4d9e5] (this=0x2ce813b... FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory Abort trap: 6 

已经尝试了节点4.2.4和5.6.0

所以,问题是为什么内存不足?

我尝试了一些东西。

  1. 我尝试join块

    而不是追加output无限期我试着检查是否大于一些大小(如8K)。 如果是这样,我把它放在一个数组,并重置输出到空string。

    通过这样做, output不会超过8K大。 数组持有180meg +簿记。 所以180meg + 8k比180meg + 180meg要less很多。 它仍然用完内存。 现在,在这个过程结束的时候,我join了这个数组,实际上它会使用更多的内存(180meg + 180meg + bookeeping)。 但是,在它到达那条线之前,v8崩溃了。

  2. 我试着改变编码只是

     function encode(num) { return 'X'; } 

    在这种情况下,它实际上运行到完成! 所以我想:“哈哈!这个问题肯定是与lookup[num]每次调用产生一个新的string有关的东西?所以我试着…

  3. lookup更改为string数组

     var lookup = Array.prototype.map.call( 'superCaliFragilisticExpialidosiousThispartdoesnotrealllymattersd', function(c) { return c; }); 

    仍然耗尽内存

这似乎是一个在V8中的错误? 虽然#2和#3是奇怪的,但它在内存使用方面看起来是相同的,所以它不能以一些奇怪的方式来GC未使用的string。

为什么v8在这些情况下耗尽内存? (并有一个解决方法)

TL; DR:您的示例是v8内部string表示之一的病态。 你可以在一段时间内通过索引output来修复它(有关下面的原因的信息)。

首先,我们可以使用heapdump来查看垃圾收集器的function:

在这里输入图像描述

上面的快照是在节点内存不足之前进行的。 正如你所看到的,大多数情况看起来很正常:我们看到两个string(非常大的output和小块被添加),三个相同的数组引用(大约64MB,类似于我们所期望的),以及许多较小的物品看起来并不特别。

但是,有一件事情是突出的: output是高达1.4+ GB。 在拍摄快照时,大概有八千万个字符,假设每个字符有两个字节,大约为160 MB。 这怎么可能?

也许这与v8的内部string表示forms有关。 引用mraleph :

有两种types的[v8string](实际上更多,但是对于目前的问题,只有这两个是重要的):

  • 扁平string是不可变的字符数组
  • consstring是string对,结合的结果。

如果连接a和b,则会得到表示串联结果的cons串(a,b)。 如果你后来连接到你会得到另一个串((a,b),d))。

索引到这样一个“树状”string不是O(1),所以为了使速度更快V8平整string时索引:将所有字符复制到一个扁平的string。

那么v8是不是可以把output当成一棵巨大的树呢? 一种检查方法是强制v8将string弄平(如上面的mraleph所示),例如通过在for循环内定期编译output

 if (i % 10000000 === 0) { // We don't do it at each iteration since it's relatively expensive. output[0]; } 

事实上,程序成功运行!

还有一个问题:为什么上面的版本2运行? 看来在这种情况下,v8能够优化掉大部分string连接(所有在右边的string连接,它们被转换为4元素数组上的按位运算)。