进程内存不足时删除大型Javascript对象

Posted

技术标签:

【中文标题】进程内存不足时删除大型Javascript对象【英文标题】:Deleting large Javascript objects when process is running out of memory 【发布时间】:2013-12-05 08:41:55 【问题描述】:

我是这种javascript的新手,所以我将做一个简单的解释:

我有一个内置于Nodejs 的网络爬虫,它收集(相当多的)数据,用Cheerio 处理它(基本上jQuery 用于Node)创建一个对象然后将其上传到mongoDB。

它工作得很好,除了在较大的网站上。 似乎正在发生的事情是:

    我给爬虫一个网上商店的 URL 来爬取 节点转到该 URL 并检索 5,000 - 40,000 个产品 URL 以进行抓取 对于每个新 URL,Node 的 request 模块获取页面源,然后将数据加载到 Cheerio。 我使用 Cheerio 创建了一个代表产品的 JS 对象。 我将对象发送到 MongoDB 并保存到我的数据库中。

正如我所说,这种情况发生在数千个 URL 上,一旦我加载了 10,000 个 URL,我就会在 node.js 中遇到错误。最常见的是:

Node: Fatal JS Error: Process out of memory

好的,这是实际的问题:

认为这是因为 Node 的垃圾清理工作不正常。例如,从所有 40,000 个 url 中抓取的 request 数据可能仍在内存中,或者至少创建的 40,000 个 javascript 对象可能仍在内存中。也许这也是因为 MongoDB 连接是在会话开始时建立的,并且从未关闭(我只是在所有产品完成后手动关闭脚本)。这是为了避免在我每次登录新产品时打开/关闭连接。

要真正确保它们被正确清理(一旦产品进入 MongoDB,我就不再使用它并且可以从内存中删除)我可以/应该只是简单地从内存中删除它,只需使用 delete product

此外(我显然不了解 JS 如何处理对象)如果我删除一个对该对象的引用,它是从内存中完全擦除,还是必须全部删除?

例如:

var saveToDB = require ('./mongoDBFunction.js');

function getData(link)
    request(link, function(data)
        var $ = cheerio.load(data);
        createProduct($)
    )


function createProduct($)   
    var product = 
        a: 'asadf',
        b: 'asdfsd'
        // there's about 50 lines of data in here in the real products but this is for brevity
        
    product.name = $('.selector').dostuffwithitinjquery('etc');
    saveToDB(product);


// In mongoDBFunction.js

exports.saveToDB(item)
    db.products.save(item, function(err)
        console.log("Item was successfully saved!");
        delete item; // Will this completely delete the item from memory?
    )

【问题讨论】:

【参考方案1】:

delete 在 javascript 中不用于删除变量或释放内存。它仅用于从对象中删除属性。您可能会在 delete 运算符上找到 this article 值得一读。

您可以通过将变量设置为null 之类的值来删除对变量中保存的数据的引用。如果没有其他对该数据的引用,那么这将使它有资格进行垃圾收集。如果还有对该对象的其他引用,则在没有更多对该对象的引用之前,它不会从内存中清除(例如,您的代码无法访问它)。

至于导致内存积累的原因,有很多可能性,我们无法真正看到足够多的代码来知道可以保留哪些引用,从而阻止 GC 释放东西。

如果这是一个长时间运行且没有中断执行的进程,您可能还需要手动运行垃圾收集器,以确保它有机会清理您已释放的内容。

这里有几篇关于在 node.js 中跟踪内存使用情况的文章:http://dtrace.org/blogs/bmc/2012/05/05/debugging-node-js-memory-leaks/ 和 https://hacks.mozilla.org/2012/11/tracking-down-memory-leaks-in-node-js-a-node-js-holiday-season/。

【讨论】:

有趣。所以在上面的例子中我需要nullmongoDBFunction.js中的变量以及在发送到saveToDB()函数后的主脚本?让我感到困惑的是对象的传递,试图计算出实际存在多少数据“副本”。 您通常不必null 任何东西,只要您不在数组或类似的东西中累积数据引用。垃圾收集器(当给定周期运行时)将处理事情。除非 DB 或 cheerio 库在内存中缓存或存储东西,或者除非您在自己的数组中累积所有数据,否则我猜您可能需要手动运行 GC。我会首先尝试手动运行 GC。如果这不起作用,请使用其中一种工具来显示哪些对象正在使用内存。 @Jascination - 还请记住,javascript 通过引用传递数组、对象和字符串等数据(它不会因为它们被传递而制作它们的新副本)。 @Jascination - 您在进行所有处理时时常运行它(例如调用global.gc())。例如,您可能每 200 个 URL 运行一次。你不想太频繁地调用它,因为它会减慢速度,但你显然不能等待太久,否则内存可能会堆积起来等待被释放。如果您想准确确定运行它的频率,则必须进行一些内存分析。这里的关键是每隔几百个 URL 运行一次,看看你的内存使用问题是否消失(或变得更好)。然后,您可以考虑优化调用它的频率。 @Jascination - 我应该补充一点,我们不知道运行垃圾收集器是问题所在。你需要运行这个实验来确定它是否能让你放松。【参考方案2】:

JavaScript 有一个垃圾收集器,可以自动跟踪哪个变量是“可访问的”。如果一个变量是“可达的”,那么它的值就不会被释放。

例如,如果您有一个全局变量 var g_hugeArray 并为其分配了一个巨大的数组,那么您实际上有两个 JavaScript 对象:一个是保存数组数据的巨大块。另一个是窗口对象上的一个属性,其名称为“g_hugeArray”,它指向该数据。所以引用链是:window -> g_hugeArray -> 实际数组。

为了释放实际数组,您使实际数组“无法访问”。您可以打破上述链中的任何一个链接来实现这一点。如果将 g_hugeArray 设置为 null,则会断开 g_hugeArray 与实际数组之间的链接。这使得数组数据无法访问,因此它将在垃圾收集器运行时被释放。或者,您可以使用“delete window.g_hugeArray”从窗口对象中删除属性“g_hugeArray”。这会破坏 window 和 g_hugeArray 之间的链接,并且还会使实际数组无法访问。

当您“关闭”时,情况会变得更加复杂。当你有一个引用局部变量的局部函数时,就会创建一个闭包。例如:

function a()

    var x = 10;
    var y = 20;
    setTimeout(function()
        
            alert(x);
        , 100);

在这种情况下,即使在函数“a”返回后,仍然可以从匿名超时函数访问局部变量 x。如果没有 timeout 函数,那么一旦函数 a 返回,局部变量 x 和 y 都将变得不可访问。但是匿名函数的存在改变了这一点。根据 JavaScript 引擎的实现方式,它可能会选择同时保留变量 x 和 y(因为在函数实际运行之前它不知道函数是否需要 y,这发生在函数 a 返回之后)。或者如果它足够聪明,它只能保留 x。想象一下,如果 x 和 y 都指向大的东西,这可能是个问题。所以关闭非常方便,但有时它更可能导致内存问题,并且可能使跟踪内存问题变得更加困难。

【讨论】:

【参考方案3】:

我在具有类似功能的应用程序中遇到了同样的问题。我一直在寻找内存泄漏或类似的东西。我的进程消耗的内存大小已达到 1.4 GB,这取决于必须下载的链接数。

我注意到的第一件事是,在手动运行垃圾收集器后,几乎所有内存都被释放了。我下载的每个页面大约需要 1 MB,经过处理并存储在数据库中。

然后我安装heapdump 并查看应用程序的快照。有关内存分析的更多信息,请访问Webstorm Blog。

我的猜测是,当应用程序运行时,GC 并没有启动。为此,我开始运行带有标志--expose-gc的应用程序,并在程序执行时开始手动运行GC。

const runGCIfNeeded = (() => 
    let i = 0;
    return function runGCIfNeeded() 
        if (i++ > 200) 
            i = 0;

            if (global.gc) 
                global.gc();
             else 
                logger.warn('Garbage collection unavailable. Pass --expose-gc when launching node to enable forced garbage collection.');
            
        
    ;
)();

// run GC check after each iteration
checkProduct(product._id)
    .then(/* ... */)
    .finally(runGCIfNeeded)

【讨论】:

【参考方案4】:

有趣的是,如果在全局范围内定义某些东西时不使用constletvar 等,它似乎是全局对象的一个​​属性,删除返回true。这可能会导致它被垃圾收集。我是这样测试的,它似乎对我的内存使用产生了预期的影响,如果这不正确或者你得到了截然不同的结果,请告诉我:


x = [];
process.memoryUsage();
i = 0;
while(i<1000000) 
    x.push(10.5);

process.memoryUsage();
delete x
process.memoryUsage();

【讨论】:

以上是关于进程内存不足时删除大型Javascript对象的主要内容,如果未能解决你的问题,请参考以下文章

使用大型数据结构时,避免 Java(eclipse) 中的“内存不足错误”?

HSQL 和 Hibernate - 许多删除语句上的内存不足错误

Powershell - 针对大型目录运行脚本时出现内存不足错误

在 PHP 中执行大型 SQL 查询字符串时出现“内存不足”错误

安卓手机下载软件总是提示内存不足,怎么办?

如何解决 用于 actionscript 3.0 编译的 java 虚拟机内存不足