对象的大数组JSON.parse()使用更多的内存比它应该的方式

我生成一个〜200'000元素的对象数组(使用map内部的对象文字符号,而不是new Constructor() ),并保存一个JSON.stringify的d版本到磁盘,它占用了31 MB包括换行符和每个缩进级别( JSON.stringify(arr, null, 1) )。

然后,在一个新的节点进程中,我将整个文件读入一个UTF-8string并传递给JSON.parse

 var fs = require('fs'); var arr1 = JSON.parse(fs.readFileSync('JMdict-all.json', {encoding : 'utf8'})); 

根据小牛的活动监视器,节点内存使用量约为1.05GB! 即使在terminal上键入,我感觉在我的古老的4 GB RAM机器上还是比较懒的。

但是,如果在一个新的节点进程中,我将文件的内容加载到一个string中,在元素边界处JSON.parseJSON.parse每个元素,表面上获得相同的对象数组:

 var fs = require('fs'); var arr2 = fs.readFileSync('JMdict-all.json', {encoding : 'utf8'}).trim().slice(1,-3).split('\n },').map(function(s) {return JSON.parse(s+'}');}); 

节点只使用〜200 MB的内存,并没有明显的系统滞后。 这种模式在节点的许多重新启动中持续存在: JSON.parse整个数组在parsing元素的过程中占用了一大笔内存,而且更高效地处理内存。

为什么在内存使用上有如此巨大的差距? 这是与JSON.parse防止高效隐藏类生成在V8中的问题? 如何在没有切分string的情况下获得良好的内存性能? 我必须使用stream式JSONparsing吗?

为了便于实验,我已经将JSON文件置于Gist中 ,请随时克隆它。

有几点需要注意:

  1. 您已经发现,无论出于何种原因,对数组的每个元素(而不是一个大的JSON.parse()进行单独的JSON.parse()调用会更有效率。
  2. 你正在生成的数据格式是在你的控制之下。 除非我误解,否则整个数据文件不一定是有效的JSON,只要你可以parsing它。
  3. 这听起来像是第二个更有效的方法唯一的问题是拆分原始生成的JSON的脆弱性。

这提示了一个简单的解决scheme:不是生成一个巨大的JSON数组,而是为数组的每个元素生成单独的JSONstring – 在JSONstring中没有换行符,即只使用没有space参数的JSON.stringify(item) 。 然后将这些JSONstring与换行符(或者您知道的任何字符将永远不会出现在您的数据中)并写入该数据文件。

当您读取这些数据时,将新来的数据拆分,然后对每一行分别执行JSON.parse() 。 换句话说,这一步就像你的第二个解决scheme,但是用一个简单的string分割,而不是不得不摆弄字符数和花括号。

你的代码可能看起来像这样(真的只是你发布的简化版本):

 var fs = require('fs'); var arr2 = fs.readFileSync( 'JMdict-all.json', { encoding: 'utf8' } ).trim().split('\n').map( function( line ) { return JSON.parse( line ); }); 

正如您在编辑中指出的那样,您可以将此代码简化为:

 var fs = require('fs'); var arr2 = fs.readFileSync( 'JMdict-all.json', { encoding: 'utf8' } ).trim().split('\n').map( JSON.parse ); 

但我会小心这个。 它在这种情况下确实起作用,但在更一般的情况下存在潜在的危险。

JSON.parse函数有两个参数 :JSON文本和一个可选的“reviver”函数。

[].map()函数将三个parameter passing给它调用的函数:项目值,数组索引和整个数组。

所以如果你直接传递JSON.parse ,它将被作为第一个参数调用JSON文本(如预期的那样),但是它也传递了一个“reviver”函数的编号。 JSON.parse()忽略了第二个参数,因为它不是函数引用,所以你可以在这里。 但是,你可以想象其他情况下你可能会遇到麻烦 – 所以当你传递一个你没有写入[].map()的任意函数时,三重检查是一个好主意。

我想这个问题的答案暗示了一个评论,但是我会稍微扩展一下。 所使用的1GB内存大概包含大量实际上“死亡”的数据分配(因为它已经变得无法访问,因此不再被程序真正使用),但还没有被垃圾收集器。

当使用的编程语言/技术是典型的现代技术(例如Java / JVM,c#/。NET,JavaScript)时,几乎所有处理大型数据集的algorithm都可能以这种方式产生大量的碎片。 最终GC删除它。

有趣的是,可以使用这些技术来显着减less某些algorithm产生的短暂内存分配量(通过指向string中间的指针),但是我认为这些技术在JavaScript中难以或不可能使用。