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

8

我对这种类型的JavaScript还是个初学者,所以我会简要解释一下:

我有一个基于Nodejs的Web爬虫,可以收集(相当多的)数据,并使用Cheerio(基本上是NodejQuery)处理数据,然后创建一个对象并将其上传到mongoDB。

它完全正常运行,只是在处理更大的网站时会出现问题。似乎正在发生的情况是:

  1. 我给爬虫提供一个在线商店的URL进行爬取
  2. Node访问该URL,然后检索从5,000到40,000个产品URL进行抓取
  3. 对于每个新的URL,Node的request模块获取页面源代码然后将数据加载到Cheerio中。
  4. 使用Cheerio创建代表该产品的JS对象。
  5. 我将这个对象发送到MongoDB,然后保存到我的数据库中。

就像我说的那样,它会处理成千上万个URL,一旦达到,比如10,000个URL被加载时,就会在node中出现错误。最常见的错误是:

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?
    })
}
4个回答

12
在JavaScript中,delete不用于删除变量或释放内存。它仅用于从对象中删除属性。您可能会发现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/


有趣。所以在上面的例子中,我需要在mongoDBFunction.js和主脚本中将变量设置为null,在它被发送到saveToDB()函数之后?让我感到困惑的是对象的传递方式,我试图弄清楚实际上有多少个数据的“副本”。 - JVG
1
只要你不在数组或类似的东西中累积数据引用,通常不需要将任何内容设置为 null。垃圾回收器(在给定周期内运行)会处理这些事情。除非 DB 或 cheerio 库正在缓存或存储内存中的内容,或者除非您正在将所有数据累积到自己的数组中,否则我猜您可能需要手动运行 GC。我首先会尝试手动运行 GC。如果那样不起作用,那么使用其中一个工具,它将向您显示正在使用内存的对象。 - jfriend00
1
@Jascination - 也请记住,JavaScript 通过引用传递数据,如数组、对象和字符串(它们在传递时不会创建新副本)。 - jfriend00
我有点困惑何时/在哪里运行垃圾收集器。 我是在主脚本中启动它一次,还是每次想要进行清理时都运行 global.gc(); - JVG
1
@Jascination - 在处理所有数据的同时,定期运行它(例如调用 global.gc())。例如,你可以在处理200个URL时运行它。不要过于频繁地调用它,因为这会减慢速度,但显然也不能等太久,否则内存可能会积累并等待被释放。如果您想确定确切的运行频率,则需要进行一些内存分析。关键在于每隔几百个URL就运行它,并查看您的内存使用问题是否得到解决(或变得更好)。然后,您可以考虑优化如何调用它的频率。 - jfriend00
1
@Jascination - 我们不确定运行垃圾回收器是否是问题。你需要运行这个实验来确定它是否能给你带来帮助。 - jfriend00

3

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

例如,如果你有一个全局变量var g_hugeArray,并且你给它分配了一个巨大的数组,实际上你在这里有两个JavaScript对象:一个是保存数组数据的巨大块。另一个是window对象上的一个名为“g_hugeArray”的属性,它指向该数据。因此,引用链是:window -> g_hugeArray -> 实际数组。

为了释放实际的数组,你可以使实际的数组“不可达”。你可以打破上面的链中的任一链接来实现这一点。如果你将g_hugeArray设置为null,则断开了g_hugeArray和实际数组之间的链接。这使得数组数据不可达,因此在垃圾回收器运行时将被释放。或者,你可以使用“delete window.g_hugeArray”从window对象中删除属性“g_hugeArray”。这会打破window和g_hugeArray之间的链接,并使实际数组不可达。

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

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

在这种情况下,即使函数"a"已经返回,本地变量x仍可以被匿名超时函数访问。如果没有超时函数,则当函数a返回时,本地变量x和y都将变得无法访问。但是,匿名函数的存在改变了这一点。根据JavaScript引擎的实现方式,它可以选择保留变量x和y(因为它不知道函数是否需要y,直到函数实际运行之后才会发生,在函数a返回之前)。或者如果它足够聪明,它只能保留x。想象一下,如果x和y都指向大型对象,这可能是个问题。因此,闭包非常方便,但有时更容易导致内存问题,并且可能会使跟踪内存问题更加困难。

2

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

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

然后我安装了heapdump并查看了应用程序的快照。有关内存分析的更多信息,请参阅Webstorm博客

enter image description here

我猜测在应用程序运行时,垃圾回收器不会启动。为此,我开始使用标志--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)

0
有趣的是,如果您在全局范围内定义某些内容时不使用constletvar等关键字,它似乎成为全局对象的属性,并且删除操作返回true。这可能导致它被垃圾回收。我进行了如下测试,它似乎对我的内存使用产生了预期的影响,请告诉我是否不正确或者您得到了截然不同的结果:

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

1
当您不使用const、let或var时,变量会绑定到全局对象(在Node.js中为"this",在浏览器中为"window"),这就是为什么它能够工作的原因! - Qgruber

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接