美文网首页
前端进阶全栈 - Node的内存管理

前端进阶全栈 - Node的内存管理

作者: stevekeol | 来源:发表于2020-01-05 03:13 被阅读0次

    0.背景

    • 如网页应用,命令行工具等短时间执行的场景,随着进程的退出,内存会释放,几乎没有内存管理的必要。
    • 基于无阻塞,事件驱动的node服务,具有内存消耗低的优点,非常适合处理海量的网络请求。

    在服务端,资源向来都是寸土寸金,要为海量用户服务,就要使一切资源都要高效循环利用。在海量请求和长时间运行下,内存控制尤为重要!

    一.V8的内存限制(内存的事实现状)

    Node中通过javascript使用内存并不能达到物理内存的上限,实际上使用的在64位系统下约1.4G,32位系统下约0.7G。

    因为Node基于V8创建,V8有自己的方式分配和管理内存。

    • 表层原因:V8最初为浏览器而设计,不太可能遇到使用大量内存的场景;
    • 深层原因: V8的垃圾回收机制的限制(以“非增量式”的回收1.5G的堆内存需要javascript线程暂停要1秒以上)

    二.V8的对象分配(何时会遇到内存限制的状况?)

    V8的所有jacascript对象都是通过堆来进行分配的。

    当我们在代码中声明变量并赋值时,所使用的对象的内存就分配在堆内存中。如果已经申请的堆内存不够分配新的对象,则继续申请堆内存,直到堆的大小超过V8的限制。

    //查看进程的内存使用量
    process.memoryUsage();
    { rss: 23236608, //常驻内存
      heapTotal: 9682944, //申请到的堆内存
      heapUsed: 5559752, //已使用的堆内存
      external: 10132
    }
    
    //查看系统的内存使用量
    > os.totalmem();
    8418570240
    > os.freemem();
    2272493568
    

    当然,V8在Node启动时可通过传递参数,来突破该内存限制:

    node --max-old-space-size=1700 index.js //1700MB
    //or
    node --max-new-space-size=1024 index.js //1400KB
    

    在Node运行时,不能动态修改!

    三.V8的垃圾回收机制(V8为何有内存限制?)

    V8的垃圾回收策略主要是分代式垃圾回收机制。所谓分代,即将内存分为新生代和老生代两代。

    • 新生代中的对象:存活时间较短的对象;
    • 老生代中的对象:存活时间较长的对象或常驻内存的对象。

    V8堆的大小 = 新生代内存空间 + 老生代内存空间

    1.新生代的垃圾回收(Scavenge算法)

    解析:

    • 新生代内存空间均分为二,每部分成为semispace。在某一时刻只有一个处于使用中(From空间),另一个处于闲置状态(To空间);
    • 分配对象时,先在From空间进行分配;
    • 开始垃圾回收时,检查From空间的存活对象并分配到To空间,非存活对象占用的空间被释放,完成复制后,From空间和To空间角色互换。

    V8堆的大小 = 新生代内存空间(From空间 + To空间) + 老生代内存空间

    评价:

    • 典型的牺牲空间换时间:仅仅使用了新生代堆内存空间的一半,(由于生命周期短的场景中存活对象只占少部分)因此复制时时间效率很高;
    2.新生代对象向老生代对象的'晋升'

    当一个对象复制多次依然存活时,将会被认为时生命周期长的对象。该对象随后(检查From空间时)会被移动到老生代中,晋升流程:

    • From空间中,该对象是否经历过Scavenge回收?是,进入老生代;否,进入To空间;
    • From空间中,判断To空间使用率是否超过25%?是,进入老生代;否,进入To空间;
    3.老生代的垃圾回收(Mark-Sweep & Mark-Compact)

    老生代中垃圾回收是将Mark-Sweep和Mark-Compact相结合。

    Mark-Sweep(标记清除)算法解析:

    • 标记阶段:遍历堆中所有对象,并标记活着的对象;
    • 清除阶段: 只清除没有标记的对象。

    评价:

    • 最大的问题是每次标记清除回收空间后,内存空间会出现不连续。这种内存碎片会对随后的内存分配造成问题,假如需要分配一个大对象,如果所有碎片空间都无法完成此次分配,则会提前触发不必要的垃圾回收。

    Mark-Compact(标记整理)算法解析:

    • 标记阶段:编理堆中所有对象,标记死亡对象;
    • 整理阶段: 将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存;
    4. 增量标记

    '全停顿':以上垃圾回收的三种方式中,为了避免javascript逻辑和垃圾回收器看到的不一致,需要将javascript暂停下来,执行完垃圾回收后在恢复执行javascript逻辑。

    为了降低该停顿时间,V8在标记阶段,将原本需要一口气停顿完成的动作改为'增量标记':拆分成许多小“步进”,每完成一个小步进,就让javascript逻辑执行一会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。

    此外,在清除和整理阶段,V8也逐步引入增量式。

    5. 查看垃圾回收的效率

    (待)

    四. 高效使用内存(实际编码中,如何让垃圾回收机制高效工作呢?)

    在正常的javascript执行中,无法立即回收的内存有闭包和全局变量引用。因为这两种将导致老生代中的对象增多。

    1.闭包

    通常作用域链上的对象只能向上访问,外部无法访问内部。

      var local = '局部变量';
      (function() {
        console.log(local);
      }())
    //正常打印
    
      (function() {
        var local = '局部变量';
      }())
      console.log(local);
    //抛出异常
    

    '闭包': javascript中,实现外部作用域访问内部作用域中变量的方法叫做闭包。

    这得益于高阶函数的特性:可以作为参数或返回值。

      var bar = function() {
        var local = '局部变量';
        return function() {
          return local;
        }
      }
    
      var baz = bar();
      console.log(baz());
    

    特点是:

    • bar()的返回值是一个匿名函数,该函数具备了访问local的条件;
    • 若要访问local,只要通过bar()这个中间函数稍作周转即可;

    闭包对内存回收机制的影响:
    一旦有变量引用这个中间函数,该中间函数将不会释放,同时也会是原始的作用域不会得到释放,作用域中产生的内存占用也得不到释放。除非不再引用,才会逐步释放。

    2.作用域

    如果变量是全局变量(不通过var声明或直接定义在global变量上),由于全局作用域需要直到进程退出才会释放,此时将导致引用的对象常驻内存(即常驻在老生代内存中)。

    只有通过delete删除引用关系或将变量重新赋值,让旧的对象脱离引用关系,才能在接下来的老生代内存清除和整理的过程中,被回收释放。

    global.foo = 'globalData';
    console.log(global.foo);
    delete global.foo;
    //或者
    global.foo = undefined/null;
    console.log(global.foo); //此后即可垃圾回收
    

    五. 堆外内存(引申)

    通过process.memoryUsage()的结果可看出,堆中内存使用量总是小于进程的常驻内存量。

    实际上,Node的内存,主要是通过V8进行分配的堆内存和Node自行分配的部分(即'堆外内存')。受V8垃圾回收限制的主要是V8的堆内存。

    var useMem = function() {
      var size = 200 * 1024 * 1024;
      var buffer = new Buffer(size);
      for(var i = 0; i < size; i ++) {
        buffer[i] = 0;
      }
      return buffer;
    }
    //headTotal 3.86MB heapUsed 2.07MB rss 11.12MB
    //...
    //headTotal 5.85MB heapUsed 1.85MB rss 3012.91MB
    

    以上可看出Buffer对象不同于其它对象,它不经过V8的内存分配机制,因此不会有堆内存的大小限制。也意味着利用堆内存可以突破内存限制的问题。


    注:以上均是自己技术栈的整理,仅供备忘。如需交流:stevekeol(微信号)

    相关文章

      网友评论

          本文标题:前端进阶全栈 - Node的内存管理

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