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

在进程内存不足时删除大型Javascript对象,javascript,node.js,mongodb,memory-management,memory-leaks,Javascript,Node.js,Mongodb,Memory Management,Memory Leaks,我是这种javascript的新手,所以我将给出一个简短的解释: 我有一个内置的网络刮板Nodejs,它收集(相当多的)数据,用Cheerio处理它(基本上是jQueryforNode)创建一个对象,然后将其上传到mongoDB 除了在更大的网站上,它工作得很好。似乎正在发生的是: 我给刮刀一个在线商店的URL来刮刀 节点转到该URL并检索5000-40000个产品URL中的任意位置以进行刮取 对于每个新URL,节点的请求模块获取页面源,然后将数据加载到Cheerio 我使用Cheerio创建了

我是这种javascript的新手,所以我将给出一个简短的解释:

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

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

  • 我给刮刀一个在线商店的URL来刮刀
  • 节点转到该URL并检索5000-40000个产品URL中的任意位置以进行刮取
  • 对于每个新URL,节点的
    请求
    模块获取页面源,然后将数据加载到
    Cheerio
  • 我使用Cheerio创建了一个代表产品的JS对象
  • 我将对象发送到MongoDB,并保存到我的数据库中
  • 正如我所说,这种情况会发生在数千个URL上,一旦我加载了10000个URL,节点中就会出现错误。最常见的是:

    Node: Fatal JS Error: Process out of memory
    
    好的,下面是实际问题:

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

    为了确保正确地清理它们(一旦产品进入MongoDB,我就不再使用它,并且可以从内存中删除),我可以/应该只从内存中删除它,只需使用
    删除产品

    Moreso(我显然不知道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?
        })
    }
    

    javascript中的
    delete
    不用于删除变量或释放内存。它仅用于从对象中删除属性。您可以在
    delete
    操作符上找到一个很好的读物

    通过将变量设置为类似于
    null
    的值,可以删除对变量中保存的数据的引用。如果没有对该数据的其他引用,那么这将使其符合垃圾收集的条件。如果存在对该对象的其他引用,则不会从内存中清除该对象,直到不再存在对该对象的引用(例如,代码无法访问该对象)

    至于导致内存累积的原因,有很多种可能性,我们无法真正看到足够多的代码,以了解哪些引用可能会保留在GC上,从而阻止GC释放一些内容

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


    这里有几篇文章介绍如何在node.js:和中跟踪内存使用情况。

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

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

    为了释放实际数组,您将实际数组设置为“不可访问”。您可以断开上面的任一链接来实现此目的。如果将g_HugarRay设置为null,则会断开g_HugarRay与实际数组之间的链接。这使得数组数据无法访问,因此当垃圾回收器运行时,它将被释放。或者,您可以使用“delete window.g_hugarray”从窗口对象中删除属性“g_hugarray”。这会断开window和g_Hugarray之间的链接,并使实际数组无法访问

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

    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都指向大的东西,这可能是一个问题。因此,关闭非常方便,但有时更可能导致内存问题,并使跟踪内存问题变得更加困难。

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

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

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

    我的猜测是,当应用程序运行时,GC不会启动。为此,我开始使用标志
    --expose 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)