大型对象上的JSON.parse()使用的内存比它应该的多

时间:2015-06-01 02:08:51

标签: javascript arrays json node.js parsing

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

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

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

根据Mavericks的Activity Monitor,节点内存使用量约为1.05 GB!即使打入终端,在我的古老4 GB RAM机器上也会感觉更加轻松。

但是,如果在一个新的节点进程中,我将文件的内容加载到一个字符串中,在元素边界处将其删除,并且每个元素单独JSON.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整个数组需要大量内存,而在元素方面解析它会更加节省内存。

为什么内存使用存在如此巨大的差异?这是JSON.parse阻止V8中有效隐藏类生成的问题吗?如何在没有切片和切块的情况下获得良好的内存性能?我必须使用流式JSON解析吗?

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

2 个答案:

答案 0 :(得分:8)

需要注意几点:

  1. 您已经发现,无论出于何种原因,对您的阵列中的每个元素执行单独的JSON.parse()调用,而不是一个大的JSON.parse(),效率会更高。
  2. 您正在生成的数据格式由您控制。除非我误解,否则整个数据文件不必是有效的JSON,只要你可以解析它。
  3. 听起来你的第二个更有效的方法是分裂原始生成的JSON的脆弱性的唯一问题。
  4. 这表明一个简单的解决方案:不是生成一个巨大的JSON数组,而是为数组的每个元素生成一个单独的JSON字符串 - 在JSON字符串中没有换行符,即只使用JSON.stringify(item)而没有{{1参数。然后将这些JSON字符串与换行符(或您知道将永远不会出现在数据中的任何字符)连接起来并写入该数据文件。

    当您阅读此数据时,请在换行符上拆分传入数据,然后分别在每行上执行space。换句话说,这一步就像你的第二个解决方案,但是用一个简单的字符串拆分而不是必须摆弄字符数和花括号。

    您的代码可能看起来像这样(实际上只是您发布的内容的简化版本):

    JSON.parse()

    正如您在编辑中所述,您可以将此代码简化为:

    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 ); 函数takes two arguments:JSON文本和可选的" reviver"功能

    它调用的函数的JSON.parse函数passes three arguments:项值,数组索引和整个数组。

    因此,如果直接传递[].map(),则会使用JSON文本作为第一个参数调用(正如预期的那样),但也会为&#34传递数字 ;齐磊"功能。 JSON.parse忽略第二个参数,因为它不是函数引用,所以你在这里就可以了。但是你可以想象一下你可能遇到麻烦的其他情况 - 所以当你传递一个你没有写入JSON.parse()的任意函数时,三重检查这一点总是一个好主意。

答案 1 :(得分:1)

我认为一个评论暗示了这个问题的答案,但我会稍微扩展一下。正在使用的1 GB内存可能包括大量实际上已经“死”的数据分配(因为它已经无法访问,因此不再被程序实际使用)但尚未被垃圾收集器。

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

值得注意的是,技术可用于显着减少某些算法产生的短暂内存分配量(通过指向字符串的中间部分),但我认为这些技术很难或不可能在JavaScript中使用