Skip to content

5内存控制

一个是垃圾回收:原理、如何查看;一个是内存:进程内存、堆外内存、内存泄漏原因以及排查等

1、V8的垃圾回收机制与内存限制

v8内存限制大小,为什么限制、垃圾回收机制、以及如何查看垃圾回收日志

  • 1、Node与v8
    • Lars Bak 开发高性能的虚拟机工作背景
    • 无与伦比的经历让V8一出世就超越了当时所有的JavaScript虚拟机。
  • 2、v8的内存限制
    • 64位系统下约为1.4GB,32位系统下约为0.7 GB
    • V8为何限制了内存的用量?
    • 内存使用量的查看方式
      bash
      node
      > process.memoryUsage();
      { rss: 14958592,
      heapTotal: 7195904,// v8申请到堆内存
      heapUsed: 2821496 }// 当前使用量
    • 表层原因:为V8最初为浏览器而设计,不太可能遇到用大量内存的场景。
    • 深层原因:
      • 是V8的垃圾回收机制的限制。
      • 内存越大垃圾回收时间越多
      • V8做一次小的垃圾回收需要50毫秒以上,
      • 做一次非增量式的垃圾回收甚至要1秒以上
      • 在当时的考虑下直接限制堆内存是一个好的选择。
      • 使用更多内存
        js
        node --max-old-space-size=1700 test.js // 单位为MB(老生代)
        // 或者
        node --max-new-space-size=1024 test.js // 单位为KB(新生代)
    • 我曾在自己的项目中遇到过,服务器内存过小,导致vite启动打包失败
  • 3、V8的垃圾回收机制
    • v8内存分代
      • 新生代
        • 存活时间较短的对象
        • 两个reserved_semispace_size_所构成
      • 老生代
        • 较长、常驻
    • v8的垃圾回收算法
      • Scavenge算法(新生代、时间少)
        • 采用 Cheney算法
        • 内存一分为二, 每一部分空间称为semispace,
        • 只有一个处于使用中(From空间), 另一个处于闲置状态(To空间)
        • 垃圾回收时, 会检查From中的存活对象,并且被被复制到To中,非存活对象占用的空间将会被释放。
        • 完成复制后, From空间和To空间的角色发生对换。(翻转)
        • 当一个对象经过多次复制依然存活时,生命周期较长,随后会被移动到老生代中(晋升)
        • 晋升两个条件:对象是否经历过Scavenge回收, 一个是To空间的内存占用比超过限制(超过25%)
      • Mark-Sweep & Mark-Compact
        • Mark-Sweep 标记清除
          • 标记活着对象和清除没有被标记的对象
          • 问题:内存碎片、不连续状态、不好分配大对象
        • Mark-Compact 标记整理
          • 解决sweep问题
          • 对象在标记为死亡后,
          • 在整理的过程中,将活着的对象往一端移动,移动完成后,
          • 直接清理掉边界外的内存。
        • v8中两种方法结合使用
      • Incremental Marking 增量标记
        • 以上三种算法执行时,应用逻辑暂停,为“全停顿”(stop-the-world)
        • 老生代影响较大
        • 解决:一口气停顿完成的动作,拆分为小“步进”
        • 做一“步进”停下,就让应用逻辑执行,交替执行
      • 延迟清理(lazy sweeping) 与增量式整理(incremental compaction)
  • 4、查看垃圾回收日志
    • --trace_gc 看耗时

      • node --trace_gc -e "var a = [];for (var i = 0; i < 1000000; i++) a.push(new Array(100))"
      • 找出垃圾回收的哪些阶段比较耗时
    • --prof

      • node --prof test01.js(5.1.js) //放一个1000000的for循环
      js
      for (var i = 0; i < 1000000; i++) {
        var a = {};
      }
      • 得到一个v8.log日志文件,不具备可读性
      • linux-tick-processor v8.log
      • windows-tick-processor.bat v8.log
        js
        // 垃圾回收所占的时间为5.4%
        [GC]:
        ticks total nonlib name
        2     5.4%

2、高性能使用内存

了解作用域、闭包以更好的使用内存

  • 1、作用域 scope
  • 2、闭包
js
// 一旦有变量引用这个中间函数, 这个中间函数将不会释放, 同时也会使原始的作用域不会得到释放, 
var foo = function () {
  var bar = function () {
    var local = "局部变量";
    return function () {
        return local;
    };
  };
  var baz = bar();
  console.log(baz());
};

3、内存指标

进程内存(v8堆内存)以及如何查看、堆外内存 1、查看内存使用

  • 查看进程内存

    • process.memoryUsage()(5.3.1.js)
    js
    var showMem = function () {
      var mem = process.memoryUsage();
      var format = function (bytes) {
        return (bytes / 1024 / 1024).toFixed(2) + " MB";
      };
      console.log(
        "Process: heapTotal " +
          format(mem.heapTotal) +
          " heapUsed " +
          format(mem.heapUsed) +
          " rss " +
          format(mem.rss)
      );
      console.log("--------------------------------------------------------");
    };
    var useMem = function () {
      var size = 20 * 1024 * 1024;
      var arr = new Array(size);
      for (var i = 0; i < size; i++) {
        arr[i] = 0;
      }
      return arr;
    };
    var total = [];
    for (var j = 0; j < 15; j++) {
      showMem();
      total.push(useMem());
    }
    showMem();
    • rss是resident set size的缩写, 即进程的常驻内存部分
    • heapTotal 1367.99 MB heapUsed 1361.86 MB rss 1375.00 MB
  • 查看系统内存

    • os模块中的totalmem()和freemem()
    js
    node
    > os.totalmem()
    8589934592
    > os.freemem()
    4527833088
    >

2、堆外内存(5.3.2.js)

  • 使用buffer做测试案例
  • Process: heapTotal 5.85 MB heapUsed 1.85 MB rss 3012.91 MB
  • heapTotal与heapUsed的变化极小,唯一变化的是rss的值
  • Buffer对象,它不经过V8的内存分配机制,不会有堆内存的大小限制。

4、内存泄漏

内存泄漏会出现在哪些方面:缓存、队列、作用域,以及解决方案

  • 缓存

  • 队列消费不及时

  • 作用域未释放

  • 1、慎将内存当做缓存

    • 常用对象的键值对来缓存东西
    • 加上完善的过期策略以防止内存无限制增长,可以使用
      1. 缓存限制策略,
      • 加一种策略限制无限增长,limitablemap一旦超出就先进先出淘汰
      • 模块机制中,模块编译后都会缓存,写模块时避免对象/数组无限制增长,添加清空队列的相应接口
      1. 缓存解决方案
      • 将缓存转移到外部:Redis、Memcached
      • 进程之间可以共享缓存。
  • 2、关注队列状态(数组)

    • 日志收集
      • 表层解决方案:是换用消费速度更高的技术,换数据库为文件写入
      • 深度:监控队列长度,一旦堆积、报警;任意异步调用包含超时机制

5、内存泄漏排查

内存泄漏排查工具,如何使用

  • node-heapdump
  • node-memwatch
  • 模块较久更新,现在应该有更新的排查工具

6、大内存应用

如何处理大文件:stream pipe

  • stream模块处理大文件(5.6.js)

    js
    const fs = require("fs");
    // var reader = fs.createReadStream("in.txt");
    // var writer = fs.createWriteStream("out.txt");
    // reader.on("data", function (chunk) {
    //   writer.write(chunk);
    // });
    // reader.on("end", function () {
    //   writer.end();
    // });
    
    // or
    var reader = fs.createReadStream("in.txt");
    var writer = fs.createWriteStream("out.txt");
    reader.pipe(writer);
  • fs的createReadStream()和createWriteStream()

  • 管道方法pipe()

  • 现在有功能更丰富的api,查阅node对应文档

实际案例回顾

  • 背景:
    • 项目(直播平台),为了更好地seo,从前端项目升级为node项目,使用nuxt框架,单页面升级到ssr,
  • 代码迁移工作:
    • nuxt支持vue,将之前代码js vue vuex状态管理,一并迁移,
    • 未更新vuex状态管理使用方法,
    • 导致数据错乱,一个直播间,会闪现别的直播间的数据
    • 为什么未更新vuex状态管理使用方法就导致这个问题?
  • 分析:
    • 1、由原前端浏览器环境,变为node服务器环境,对于访问用户而言,原来这套js每个浏览器下载一遍,数据不共用,而现在服务器端这套js大家是共用的;
    • 2、vue状态管理的代码逻辑是,将主要的数据存储到了对象,这个对象大家是共用的,当多个用户大量访问,延时未及时更新时,就会出现错乱现象;
    • 3、更新vuex的使用方法,vuex中存储的对象,改为函数调用返回一个新对象,做到数据的隔离,则问题解决