Node:内存控制学习笔记

本文详细探讨了Node.js中V8引擎的内存限制、对象分配、垃圾回收机制,包括Scavenge算法、Mark-Sweep & Mark-Compact等,并介绍了如何高效使用内存、查看内存使用情况以及如何避免内存泄漏,旨在帮助开发者更好地理解和优化Node.js应用的内存管理。
摘要由CSDN通过智能技术生成

五、内存控制

  • 基于无阻塞,事件驱动建立的Node服务,具有内存消耗低的优点,非常适合处理海量的网路请求,而内存控制就是在海量网络请求和长时间运行的前提下探讨的。

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

  • 与Java一样,JavaScript由垃圾回收机制来及逆行自动内存管理。
1.1 V8的内存限制
  • Node通过JavaScript只能使用部分的内存:64位系统下为1.4GB,32位系统下位0.7GB。
  • 在这样的限制下,Node无法直接操作大内存对象,比如无法将一个2GB的文件读入内存进行字符串分析处理。
  • 这个问题的起因在于Node基于V8引擎构建,而V8在的内存管理在处理浏览器场景下绰绰有余,足以胜任前端页面的所有需求。
  • 尽管服务器中大内存的场景不是很常见,但是一旦内存触碰了界限,就会导致进程退出。
1.2 V8的对象分配
  • 在V8中,所有JavaScript对象都是通过堆来分配的。

  • Node提供了V8中内存使用量的查看方式,通过以下代码执行:

    node 
    > process.memoryUsage();
    {
         
      rss: 24289280,
      heapTotal: 4694016,
      heapUsed: 2798016,
      external: 1548986,
      arrayBuffers: 230581
    }
    
    
  • 在返回的属性中,我们可以看到堆的总内存和已被使用的内存。

  • 当我们在代码中声明变量或者赋值时候,所使用的对象的内存就被分配在堆中。

  • 如果已经申请的堆空闲内存不够分配新的对象,则需要继续申请堆内存,直到堆的大小超过V8限制。

  • V8限制堆的大小,是因为对于网页来说,V8的限制已经绰绰有余;深层原因是垃圾回收机制的限制。

  • 以1.5GB的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50ms以上,做一次非增量式的垃圾回收甚至需要1s以上。

  • 而垃圾回收也会引起JavaScript线程暂停执行,在这样的时间花销下,应用的性能和响应能力会直线下降,因此需要对堆内存进行一个限制。

  • 其实,这个限制也可以改变,Node提供了一个选项在启动时:

    node --max-old-space-size=1024 test.js
    // 或者
    node --max-new-space-size=2048 text.js
    
  • 上述参数在V8启动时生效,一旦启动就无法动态改变了。

  • 如果Node遇到内存限制的问题,可以用这个方法来放宽V8的内存限制,避免在执行过程中轻易崩溃。

1.3 V8的垃圾回收机制
1.3.1 主要垃圾回收算法
  • V8的垃圾回收策略主要基于分代式垃圾回收机制
  • 在实际的应用中,对象的生存周期长短不一,不同算法只能针对特定情况有最好的效果。
  • 现代的垃圾回收算法按对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同分代的内存使用更高效的垃圾回收算法。
1.3.2 V8的内存分代
  • V8中,内存主要分为老生代和新生代。
  • 新生代中的对象位存活时间较短的对象;老生代中的对象位存活时间较长或者常驻内存的对象。
  • V8堆的整体大小就是新生代和老生代所用的内存空间之和。前面两条命令就是用来设置分代内存的,但是只能在启动时设置,而且无法进行动态设置,意味着V8使用的内存无法根据使用情况进行动态扩张。
  • 在V8的源码中,可以找到生代内存在不同位操作系统下的初始值。
  • 在64位系统下老生代的内存为1400MB,新生代内存为32MB;
  • 在32位系统下老生代的内存为700MB,新生代内存为16MB。
1.3.3 Scavenge算法
  • 新生代内存只要通过Scavenge算法进行垃圾回收,而Scavenge算法主要采用Cheney算法。

  • Cheney算法采用复制的方式来实现垃圾回收,它将新生代内存一分为二,两边都称为semispace,一个处于使用中(From),另一个处于闲置状态(To)。

  • 算法的主要流程是这样的:

    1. 当开始进行垃圾回收时,会检查From空间的存活对象
    2. 将这些存活对象复制到To空间里,非存活对象释放空间
    3. 将From空间和To空间的角色进行对换。(这个过程称为翻转)
  • Scavenge的缺点时只能使用堆内存的一半,这是由划分空间和复制机制所决定的。但是Scavenge至复制存活对象,并非且对于生命周期短的场景,存活对象只占用小部分,所以在空间效率上有优异的表现。

  • Scavenge是典型的空间换时间的算法,所以无法大规模地引用到所有垃圾回收中,但由于新生代的对象的生命周期较短,非常适合这个算法。

  • 当一个对象被复制多次后,它就会被认定为生命周期较长的对象,随后就会被晋升到老生代的内存中。

  • 晋升有两个条件:

    1. 对象是否经历过Scavenge回收
    2. To空间占用比是否超过限制
  • 默认情况下,对象分配主要在From空间中。

  • 对象从From复制到To空间时,会检查它的地址来判断是否已经经历过一次回收,如果有,就会被复制到老生代空间中;否则复制到To空间中。

  • 当从From复制对象时,如果To空间已经使用超过了25%的空间,则这个对象直接晋升到老生代空间中。

  • 设置25%指标是因为To翻转后会变成From,如果占比过高会影响到新对象的内存分配。

1.3.4 写屏障
  • 对于新生代对象的回收,我们只需要检查指向新生代的引用,那么在跟随跟对象->新生代或者新生代->新生代的引用时,我们可以放心跟随。
  • 对于新生代->老生代或者根对象->老生代的引用,如果直接跟随,可能会由于路径过长而花费大量时间,;如果不跟随,万一有新生代对象指向老生代对象的引用,那就不能直接回收这个对象了,这样一来,新生代的GC就无法做了。
  • 对此,V8采用**写屏障(write barrier)**的方法——即每次往一个对象添加引用的时候,就执行一串代码。
  • 这个代码会检查这个被写入的指针是不是由老生代->新生代的,这样我们就能明确地直到所有从老生代->新生代的指针了。这个用于记录的数据结构称为store buffer,每个堆维护一个,定期检测、请理、更新。
  • 在新生代垃圾回收阶段,就会跳过这些被写入屏障的对象了。
1.3.5 Mark-Sweep & Mark-Compact
  • 三色标记法:

    白色代表这个对象可以被回收;

    黑色代表这个对象不能被回收,他的所有引用都被扫描完毕了;

    灰色代表这个对象不能被回收,但它产生的引用还没有被扫描完。

    1. 当老生代GC启动时,V8会扫描老生代对象,沿着引用做标记,将这些标记保留在对应的marking bitmap中。
    2. 最开始所有的非跟对象带有的标记都是白的;
    3. 接着V8把根对象引用的对象放到一个显示的栈中,并标记为灰色;
    4. 接着,V8把这些对象进行深度优先遍历,每访问一个对象就将其标记为黑色并pop,然后将其引用的所有白色对象标记为灰色,push到栈;
    5. 如此重复,直到所有对象都pop后,最后的老生代对象就只有黑白两种颜色了。然后对白色对象进行回收。
    • 注意:当对象太大无法push到栈,他会先把这个对象保留灰色,先放弃,然后将栈标记为溢出状态。随后继续pop对象,直到栈空。此时V8重新遍历整个堆,把标记为灰色和溢出对象压入栈中,重新操作。
  • 老生代中,存活对象的比重大且多,复制起来效率极差,因此不适用Scavenge,而用的是Mark-Sweep & Mark-Compact相结合的方式进行垃圾回收。

  • Mark-Sweep字面意思就是标记清除法,分为两个阶段:

    1. 标记阶段。首先遍历老生代堆中的所有对象,标记存活的对象;
    2. 清除阶段。清除掉没有标记的对象,完成一次垃圾回收。
  • 可以看出,Mark-Sweep只清除死亡对象。由于死亡对象在老生代中的占比较小,因此这种清除方式非常高效。

  • 但是,Mark-Sweep最大的问题就是当清除了死亡对象后,会出现内存空间不连续的状态。这种内存碎片会对后续内存分配造成极大的影响。当需要分配一个大对象时,由于空间不足,会提前触发一次不必要的垃圾回收机制。

  • Mark-Compact的字面意思是标记整理。他与Mark-Sweep的区别是,在对象被标记成死亡后,整理过程中,将活着的对象往一端移动,移动完成后,直接请理掉边界外的内存。

1.3.6 三种垃圾回收算法的对比
算法 Scavenge Mark-Sweep Mark-Compact
速度 最快 中等 最慢
空间开销 双倍空间、无碎片 少、有碎片 少、无碎片
是否移动对象
  • 由于Mark-Compact需要移动对象,所以执行速度会变得很慢,因此V8主要采用Mark-Sweep算法进行老生代垃圾回收,当分配内存空间不足时,才会进行一次Mark-Compact整理内存碎片。
1.3.7 增量标记(increment Marking)
  • 为了避免出现应用逻辑与垃圾回收器的情况不一致,垃圾回收在进行时,需要暂停应用逻辑层,带执行垃圾回收完毕后再开恢复。这种行为称为:全停顿(stop-the-world)

  • 在V8的分代回收中,一次小的垃圾回收只收集新生代,新生代中的存活对象较少,即使全停顿也影响很小;

  • 但是老生代配置大,存活对象多,垃圾回收的标记、请理、移动会占用很多时间,因此不适合全停顿。

  • 为了降低全堆垃圾回收带来的时间停顿,V8在标记阶段使用了增量标记方法,将原本一口气标记完的动作拆分成许多小步骤,每走一小步就让JavaScript应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行,直到标记阶段完成。

1.3.8 惰性清除(Lazy sweeping)
  • 当增量标记完成后,惰性清除开始。
  • 此时所有对象都已经标记完成,堆可以准确知道可以回收多少内存,无需一次性清除所有死亡对象,可以根据需要来选择回收部分内存,直到所有垃圾对象回收完毕。
1.3.9 并行请理(parallel sweeping)
  • 由于sweeping作用的对象是被标记死亡的对象,所以主线程不会访问到他们,因此V8引入并行请理,让多个请理线程共同工作,提高sweeping的吞吐量,缩短整个GC周期。
1.4 查看垃圾回收日志
  • 查看垃圾回收日志可以通过--trace_gc参数实现:

    node --trace_gc -e "let a=[];for(let i=0;i<100;i++) a.push(new Array(100));" > gc.log
    
  • 可以在启动目录下找到gc.log文件,打开后,可以看到这些信息:

    [15900:0000028119CA8A70] 48 ms: Scavenge 2.4 (3.2) -> 2.0 (4.2) MB, 1.1 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure

  • 另外,在node启动时,使用--prof参数可以得到V8执行时的性能分析数据,包括垃圾回收时占用的时间:

    [9856:0000024A812C6170] 145 ms: Scavenge 2.1 (2.8) -> 1.6 (3.8) MB, 0.7 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure
    [9856:0000024A812C6170] 160 ms: Scavenge 2.5 (4.2) -> 2.2 (4.9) MB, 0.7 / 0.0 ms (average mu = 1.000, current mu = 1.000) allocation failure

2. 高效使用内存

2.1 作用域
  • 在JavaScript中,能形成作用域的有函数调用、with和全局作用域。
  • 当函数每次被调用时,会创建对应的函数作用域,同时作用域中声明的局部变量会被分配在该作用域上,函数执行结束后,作用域以及局部变量将会销毁。
2.1.1 标识符查找
let foo = function () {
   
    console.log(local);
}
  • JavaScript在执行时,会最先查找当前作用域内有无对应的local变量。如果没有,就会向上级作用域继续查找。
2.1.2 作用域链
var foo = 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值