美文网首页
2019-05-30

2019-05-30

作者: 平仄_pingze | 来源:发表于2019-05-30 17:30 被阅读0次

    1. V8内存管理和相关问题

    Node.js基于V8引擎,其内存管理就是V8的内存管理。

    V8内置了自动垃圾回收(GC)。

    V8由Google开发,使用C++编写,最早在Chrome中使用。相对于其他JavaScript引擎将代码装换成字节码或解释执行,V8将代码变异成原生机器码,并且使用了如内联缓存等方法来提高性能。JavaScript程序在V8引擎下运行速度媲美二进制程序。

    autoauto- 1. V8内存管理和相关问题 (原)auto - 1.1. V8内存设计auto - 1.1.1. 内存分区auto - 1.1.2. 内存生命周期auto - 1.2. V8垃圾回收auto - 1.2.1. 标记清除法auto - 1.2.2. 垃圾回收算法auto - 1.3. Node.js如何检视内存和GCauto - 1.3.1. 测试auto - 1.3.1.1. external内存和GC测试auto - 1.3.1.2. heap内存和GC测试auto - 1.3.2. 更多auto - 1.3.2.1. 总结auto - 1.4. 常见的内存泄漏案例auto - 1.4.1. 全局变量auto - 1.4.2. 闭包auto - 1.4.3. 消费者速度小于生产者auto - 1.5. 如何发现和定位内存问题auto - 1.5.1. memwatch-nextauto - 1.5.2. heapdumpauto - 1.5.3. 使用PM2做 Memory Threshold Auto Reload 处理autoauto

    1.1. V8内存设计

    1.1.1. 内存分区

    V8中,内存分为几个部分:

    • 新生代区 new space
      大多数的对象都会被分配在这里,这个区域很小但是垃圾回收比较频繁。

    • 老生代区 old space
      属于老生代,这里只保存原始数据对象,这些对象没有指向其他对象的指针。

    • 大对象区 large object space
      这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象区。

    • 代码区 code space
      代码对象,会被分配在这里。唯一拥有执行权限的内存。

    • map区 map space
      存放 Cell 和 Map,每个区域都是存放相同大小的元素,结构简单。

    1.1.2. 内存生命周期

    一个对象A创建后,被分配到新生代区。

    新生代区满后,V8进行Scavenge操作,清除需要回收的。如果对象A还有效,则保留。

    如果对象A再次被清理(或者满足其他条件),则晋升到老生代区。

    老生代区满后,V8进行Mark Sweep操作,将这时需要回收的对象A清除。

    1.2. V8垃圾回收

    1.2.1. 标记清除法

    当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。

    与之对应的还有引用计数法,但会因循环引用导致内存泄漏,所以很少见到。

    1.2.2. 垃圾回收算法

    由于新生代和老生代存放了不同性质的内存对象,其清除方式也不同。

    简单来说,新生代使用Scavenge算法。分成From和To两个区,将需要回收的对象留在From,其他移到To,然后交换From和To。垃圾回收将To空间内存全部释放。

    老生代使用Mark Sweep算法,直接标记需要被回收的对象,在垃圾回收时释放相应地址空间。

    此外,还有Mark Compact算法,将存活和需要回收的对象放在地址区域的两边,以避免回收后内存不连续的问题。

    1.3. Node.js如何检视内存和GC

    Node.js提供了一些API来帮助开发者检视程序的内存使用状况和GC情况。

    process.memoryUsage()
    

    会返回一个内存使用信息对象,单位为字节Byte。类似:

    Object {rss: 25358336, heapTotal: 8232960, heapUsed: 5488248, external: 8608}
    
    • rss 驻留集大小, 即程序分配的物理内存大小,包括堆、栈、代码段
    • heapTotal V8堆总大小
    • heapTotal V8堆使用量大小
    • external V8绑定到Javascript的C++对象的内存大小

    对象,字符串,闭包等存于堆内存。 变量存于栈内存。 实际的JavaScript源代码存于代码段内存。

    1.3.1. 测试

    下面的测试,执行时都给node添加启动参数--trace-gc--expose-gc
    前者可以打印出GC操作log,后者允许在代码中控制GC。

    1.3.1.1. external内存和GC测试

    尝试用fs.readFileSync('/path/')读取一个100M左右的文件。发现rss和external增加了100M左右。
    heapUsed则只增加了一点。看来直接读取文件返回的是一个C++对象的引用。

    即使没有保存fs.readFileSync()返回的对象,rss和external还是增大了。且即使等待,这部分内存也不会被回收。

    在代码中调用global.gc()主动进行GC回收。
    回收后,增加的100M左右rss被释放。

    1.3.1.2. heap内存和GC测试

    如果使用fs.readFileSync('/path/', 'utf-8'),返回的将是一个字符串对象。会占用heap内存。
    反复执行10次并保留每次的引用,发现程序错误:

    FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
    

    这体现了V8的堆内存大小是有限制的。这个限制可以修改。
    老生代用node --max-old-space-size=xxxx(单位MB)修改。
    新生代用node --max-new-space-size=xxxx(单位MB)修改。

    1.3.2. 更多

    • node --v8-options print v8 command line options
    • node --v8-pool-size=num set v8's thread pool size
    • node --prof-process process v8 profiler output generated using --prof
    • node --track-heap-objects track heap object allocations for heap snapshots
    • os.totalmem() 系统总内存
    • os.freemem() 系统空闲内存

    1.3.2.1. 总结

    通过测试,可以发现GC的一些表面规则:

    • 部分函数会创建C++对象并返回其引用,而不是JS对象。因此占用external而非heap。
    • global.x 不会被回收。const x,如果后面没有使用x,则会很快被回收。

    1.4. 常见的内存泄漏案例

    1.4.1. 全局变量

    全局变量global.xxx不会被GC回收。

    未声明变量会隐式产生全局变量:

    function foo() {
      // 即 global.a = 1;
      a = 1;
    }
    

    使用tslint等工具规范代码可以避免此种问题。

    1.4.2. 闭包

    闭包就是能够读取其他函数内部变量的函数。

    闭包作用域会保留其中涉及的引用,会导致对象无法被回收。

    要注意的一个知识点是:每当在同一个父作用域下创建闭包作用域的时候,这个作用域是被共享的。

    看一个经典问题(曾经是web框架meteor的著名bug):

    let theThing = null;
    const replaceThing = function () {
      const originalThing = theThing;
      function unused() {
        if (originalThing) {}
      }
      theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: () => {}
      };
    };
    setInterval(() => {
      replaceThing();
      console.log(process.memoryUsage().heapTotal)
    }, 1000);
    

    运行这段代码,会发现rss、heapTotal、heapUsed不断增长。

    这是因为在replaceThing()的词法作用域中,声明了originalThing,而闭包函数unused()使用了originalThing;theThing.someMethod()虽然是空函数,但由于上面提及的知识点,其闭包作用域也包含了originalThing,而theThing定义在文件作用域,无法回收。

    这些就导致了每次重声明的originalThing都无法回收,就会有大量longStr积累在堆中。

    如果需要解决这个问题,可以在replaceThing()的最后加originalThing = null;

    这个问题出现的关键在于,变量间产生了循环使用,且一个在闭包作用域中,导致其每次定义后,都无法释放。

    也可以改成下面这样,效果一样:

    let theThing = null;
    const replaceThing = function () {
      const originalThing = {
        theThing,
        longStr: new Array(1000000).join('*'),
      };
      function unused() {
        if (originalThing) {}
      }
      theThing = ()=> {}
    };
    setInterval(() => {
      replaceThing();
      console.log(process.memoryUsage().heapTotal)
    }, 1000);
    

    1.4.3. 消费者速度小于生产者

    常见于使用消息队列或大量IO操作时。由于作为生产者时,消费者一方不能及时处理任务,导致任务数据在生产者内存缓存中大量积存,最终导致内存溢出。

    1.5. 如何发现和定位内存问题

    1.5.1. memwatch-next

    memwatch-next是一个能发现内存泄漏问题,并给出简单问题分析的工具。

    使用如下:

    // 使用方式1:监听内存泄漏
    // 5个连续GC周期下,
    memwatch.on('leak', function (info) { 
      console.warn("MEMLEAK", info);
    });
    
    // 使用方式2:生成一段时间的内存和对象变化报告
    const hd = new memwatch.HeapDiff();
    setTimeout(() => {
      const diff = hd.end();
      console.log(JSON.stringify(diff, null, "  "));
    }, 1000 * 10);
    

    在上面那个闭包引起内存泄漏的代码中使用,可以发现部分报告输出如下:

    {
      "change": {
        "details": [
          {
            "what": "Closure",
            "size_bytes": 6624,
            "size": "6.47 kb",
            "+": 96,
            "-": 4
          },
          {
            "what": "String",
            "size_bytes": 93003520,
            "size": "88.7 mb",
            "+": 205,
            "-": 22
          }
        ]
      }
    }
    

    由此,可以推测是大量String对象造成内存占用,可能和闭包有关。

    1.5.2. heapdump

    heapdump是一个用于导出V8 Heap Snapshot的工具。导出数据可以导入到Chrome浏览器查看。

    和memwatch结合使用:

    memwatch.on('leak', function (info) { 
      console.warn("MEMLEAK", info);
      heapdump.writeSnapshot('' + Date.now() + '.heapsnapshot');
    });
    

    等到leak事件触发后,便会导出一个.heapsnapshot文件。从 [Chrome开发者工具]-[memory]-[Profiles]-[Heap snapshot] 中,Load这个文件。

    然后就可以看到报告内容。
    可以按Shallow Size排序,查看是何种对象占用了大量内存。(如果内存泄漏时缓慢增长的,则可以等待足够长时间后再导出报告)

    对上面的闭包例子做报告,可以发现占用最多的是string,有多个大体积的“***...*”字符串。

    1.5.3. 使用PM2做 Memory Threshold Auto Reload 处理

    有时内存泄漏的问题隐藏地很深,短时间内难以定位和解决。这时要优先保证服务正常运行不收内存
    问题的影响,就可以利用pm2管理工具的内存限制重启特性。

    具体方式是在配置文件中增加max_memory_restart属性:

    apps: [{
      max_memory_restart: '300M'
    }]
    

    相关文章

      网友评论

          本文标题:2019-05-30

          本文链接:https://www.haomeiwen.com/subject/quovtctx.html