【问题标题】:Node doesn't gc my object properly节点没有正确地 gc 我的对象
【发布时间】:2017-10-20 00:04:06
【问题描述】:

这是一个简单的案例:

let html = `<<some huge html file>>`
var libxmljs = require("libxmljs");

class MyObject{
  constructor(html){
    this.doc = libxmljs.parseHtml(html);
    this.node = this.doc.root()
  }
}

let obj

for(var i = 0; i < 100000; i++){
  obj = new MyObject(html)
  // if I uncomment the next line it works fine
  // obj.node = null
  console.log(i)
}

当我运行它时,脚本很快就会耗尽内存,显然是因为 obj.node 没有正确收集垃圾。当我认为我已经完成它时,如何确保在不明确将其设置为 null 的情况下发生这种情况?

【问题讨论】:

    标签: javascript node.js garbage-collection libxml-js


    【解决方案1】:

    如果您没有将引用专门存储在类实例中,则.root() 返回的对象似乎更多地出现在 GC 中。内存使用似乎仍然相当泄漏,因为分配的全部堆量从未回收。 Node 本身使用的内存似乎是堆上的两倍来处理本机 libxml 代码。也许提出一个issue on libxmljs,因为这就像一个虫子一样嘎嘎作响。

    不将对象存储在类实例中而是传递它会更好。

    class MyObject{
      constructor(){
        this.doc = libxmljs.parseHtml(html)
      }
      get node(){
        return this.doc.root()
      }
    }
    

    使用普通对象也效果更好。

    function myObject(){
      let doc = libxmljs.parseHtml(html)
      let node = doc.root()
      return {
        doc: doc,
        node: node,
      }
    }
    

    作为替代方案,可以尝试JS based parsers 之一。

    【讨论】:

    • 为什么 Node 不 gc'ing 它呢?这对我来说就像 Node.js 中的一个错误。我觉得使用析构函数可以解决我的问题,但是 Node 团队认为它不需要析构函数,而我的示例表明它可能确实需要。不过我是 Node 新手,所以我希望我忽略了一些明显的东西。
    • Node 通常会,如果你用普通的 JS 对象这样做,它会很好。 libxmljs 基于原生的 libxml2 库,这为更多内存问题开辟了可能性。基于 JS 的解析器可能会较慢,但出现此类问题的可能性较小。
    • 目前还不清楚为什么节点不能正确地 gc'ng 我的对象。听起来 node 确实需要析构函数,即使它声称不需要。析构函数可以很好地解决我的问题。
    • 这个想法是你在正常操作中不需要析构函数......就像我说的那样,libxmljs 模块在它的本机 C 数据和 JS 之间保持引用的方式看起来像一个错误对象。
    • 嗯,您对“正常操作”的使用对我来说是有问题的。我觉得这是一个正常的操作,应该有一种简单的方法来确保对象被正确地垃圾收集。我想等待其他人对此进行权衡,无意冒犯,我感谢您的回答。
    【解决方案2】:

    TL;DR:问题在于库而不是节点。

    长答案

    这里是稍加修改的代码

    var heapdump = require('heapdump');
    const fs = require('fs');
    var libxmljs = require("libxmljs");
    
    const content = fs.readFileSync('./html2.htm');
    let id = 0;
    
    class MyObject{
      constructor(){
        this.doc = libxmljs.parseHtml(content);
        this.node = this.doc.root()
      }
    }
    
    let obj;
    
    function createObject () {
      obj = new MyObject(content);
    };
    
    
    try {
      for(var i = 0; i < 3000; i++){
        createObject();
        // if I uncomment the next line it works fine
        // obj.node = null
        console.log(i);
        if (i === 50) {
          heapdump.writeSnapshot('/Users/me/3.heapsnapshot');
        }
        if (i === 100) {
          heapdump.writeSnapshot('/Users/me/4.heapsnapshot');
        }
        if (i === 150) {
          heapdump.writeSnapshot('/Users/me/5.heapsnapshot');
        }
    
      }
      console.log('done');
    }
    catch(e) {
      console.log(e);
    }
    

    下面是我们在代码中获取的 heapdump diff 的相关部分(3 和 4)

    当我们查看 4 和 5 堆转储时甚至清楚

    我们可以从这些堆转储中得出一些结论:

    • JS部分没有内存泄漏。
    • 堆转储的大小与我们在 htop/top/activity 监视器上看到的进程大小不匹配,具体取决于您的操作系统。 (12 MB 的堆转储与几 Gb 的 RAM)

    堆转储只会给我们内存泄漏which are in JS。由于这个库有 c 代码,所以 heapdump 不会捕获将存在的泄漏。

    我不确定我们如何从该库中捕获转储,或者为什么将其设置为 null 可以释放内存,但可以安全地假设 node gc 正在尽其所能。

    希望对你有帮助

    【讨论】:

    • 谢谢,这是有用的信息。我仍然不确定我是否同意 node 正在尽其所能。
    • 有趣的是,node --trace_gc 确实跟踪了 JS 堆中的一些对象保留。旧空间的增长速度约为操作系统上实际进程内存速度的 1/2。 process.memoryUsage() 确实显示相同,在 external 跟踪内存的顶部
    猜你喜欢
    • 1970-01-01
    • 2013-01-04
    • 2015-02-15
    • 2012-11-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-03
    相关资源
    最近更新 更多