Wsh's blog Wsh's blog
首页
  • 基础知识
  • ArkUI
  • UIAbility
  • 组件通信方式
  • 前端缓存
  • React
  • typescript
  • javascript
  • flutter
  • node
  • webpack
web3D😉
宝库📰
  • 分类
  • 标签
  • 归档
龙哥的大🐂之路 (opens new window)
GitHub (opens new window)

wsh

热爱前端的程序媛
首页
  • 基础知识
  • ArkUI
  • UIAbility
  • 组件通信方式
  • 前端缓存
  • React
  • typescript
  • javascript
  • flutter
  • node
  • webpack
web3D😉
宝库📰
  • 分类
  • 标签
  • 归档
龙哥的大🐂之路 (opens new window)
GitHub (opens new window)
  • 深入浅出node

    • node介绍
    • 模块机制
    • 异步I/O操作
    • 异步编程
    • 内存控制
      • 理解Buffer
      • 网络编程
      • 进程
    • nest框架

    • koa

    • node
    • 深入浅出node
    2022-05-03
    目录

    内存控制

    内存控制正是在海量请求和长时间运行的前提下进行探讨的。在服务器端,资源向来就寸土寸金,要为海量用户服务,就得使一切资源都要高效循环利用。

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

    JavaScript由垃圾回收机制来进行自动内存管理,

    # V8 的内存限制

    在Node中通过JavaScript使用内存时就会发现只能使用部分内存(64位系统下约为1.4 GB,32位系统下约为0.7 GB)。 造成这个问题的主要原因在于Node基于V8构建,所以在Node中使用的JavaScript对象基本上都是通过V8自己的方式来进行分配和管理的。V8的这套内存管理机制在浏览器的应用场景下使用起来绰绰有余,足以胜任前端页面中的所有需求。但在Node中,这却限制了开发者随心所欲使用大内存的想法.

    # V8 的对象分配

    在V8中,所有的JavaScript对象都是通过堆来进行分配的。Node提供了V8中内存使用量的查看方式,执行下面的代码,将得到输出的内存信息

    wsh@wsh ~ %  node
    > process.memoryUsage();
    {
      rss: 99205120,
      heapTotal: 6049792,
      heapUsed: 4121312,
      external: 1682060,
      arrayBuffers: 9965
    }
    >
    

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

    V8为何要限制堆的大小?

    • 表层原因: 为V8最初为浏览器而设计,不太可能遇到用大量内存的场景
    • 深层原因是V8的垃圾回收机制的限制。按官方的说法,以1.5 GB的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至要1秒以上。这是垃圾回收中引起JavaScript线程暂停执行的时间,在这样的时间花销下,应用的性能和响应能力都会直线下降。这样的情况不仅仅后端服务无法接受,前端浏览器也无法接受。因此,在当时的考虑下直接限制堆内存是一个好的选择。

    调整内存限制的大小

    node --max-old-space-size=1700 test.js // 单位为MB
    // 或者
    node --max-new-space-size=1024 test.js // 单位为KB
    

    # V8 的垃圾回收机制

    # V8主要的垃圾回收算法

    V8的垃圾回收策略主要基于分代式垃圾回收机制。

    • V8的内存分代 在V8中,主要将内存分为新生代和老生代两代。新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。

    V8的分代示意图:

    V8堆的整体大小就是新生代所用内存空间加上老生代的内存空间

    • --max-old-space-size命令行参数可以用于设置老生代内存空间的最大值,
    • --max-new-space-size命令行参数则用于设置新生代内存空间的大小
    # 新生代内存

    对于新生代内存,它由两个reserved_semispace_size_所构成。按机器位数不同,reserved_semispace_size_在64位系统和32位系统上分别为16 MB和8 MB。所以新生代内存的最大值在64位系统和32位系统上分别为32 MB和16 MB。

    V8堆内存的最大保留空间可以从下面的代码中看出来,其公式为4 * reserved_semispace_size_ + max_old_generation_size_。 因此,默认情况下,V8堆内存的最大值在64位系统上为1464 MB,32位系统上则为732 MB。这个数值可以解释为何在64位系统下只能使用约1.4 GB内存和在32位系统下只能使用约0.7 GB内存。

    • Scavenge算法 在分代的基础上,新生代中的对象主要通过Scavenge算法进行垃圾回收。在Scavenge的具体实现中,主要采用了Cheney算法. Cheney算法是一种采用复制的方式实现的垃圾回收算法。它将堆内存一分为二,每一部分空间称为semispace.

    在这两个semispace空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的semispace空间称为From空间,处于闲置状态的空间称为To空间。当我们分配对象时,先是在From空间中进行分配。当开始进行垃圾回收时,会检查From空间中的存活对象,这 些存活对象将被复制到To空间中,而非存活对象占用的空间将会被释放。完成复制后,From空间和To空间的角色发生对换。简而言之,在垃圾回收的过程中,就是通过将存活对象在两个semispace空间之间进行复制。

    V8的堆内存示意图

    Scavenge的缺点是只能使用堆内存中的一半,这是由划分空间和复制机制所决定的。但Scavenge由于只复制存活的对象,并且对于生命周期短的场景存活对象只占少部分,所以它在时间效率上有优异的表现。适合应用在新生代中,因为新生代中对象的生命周期较短,恰恰 适合这个算法

    晋升: 当一个对象经过多次复制依然存活时,它将会被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中,采用新的算法进行管理。对象从新生代中移动到老生代中的过程称为晋升。

    在分代式垃圾回收的前提下,From空间中的存活对象在复制到To空间之前需要进行检查。在一定条件下,需要将存活周期长的对象移动到老生代中,也就是完成对象晋升. 对象晋升的条件主要有两个:

    1. 一个是对象是否经历过Scavenge回收

    晋升流程: 2. 一个是To空间的内存占用比超过限制。

    晋升的判断示意图

    设置25%这个限制值的原因是当这次Scavenge回收完成后,这个To空间将变成From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配.

    # 老生代内存 (Mark-Sweep(标记清除) & Mark-Compact(标记整理))

    Mark-Sweep是标记清除的意思,它分为标记和清除两个阶段 Mark-Sweep在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。 Scavenge中只复制活着的对象,而Mark-Sweep只清理死亡对象。活对象在新生代中只占较小部分,死对象在老生代中只占较小部分。 Mark-Sweep在老生代空间中标记后的示意图:黑色部分标记为死亡的对象

    Mark-Sweep最大的问题是在进行一次标记清除回收后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题,因为很可能出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。 Mark-Compact: 在Mark-Sweep的基础上演变而来的。是为了解决Mark-Sweep的内存碎片问题。它们的差别在于对象在标记为死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。白色格子为存活对象,深色格子为死亡对象,浅色格子为存活对象移动后留下的空洞。

    完成移动后,就可以直接清除最右边的存活对象后面的内存区域完成回收。

    完成标记并移动存活对象后的示意图:

    3种垃圾回收算法的简单对比:

    从表中可以看到,在Mark-Sweep和Mark-Compact之间,由于Mark-Compact需要移动对象,所以它的执行速度不可能很快,所以在取舍上,V8主要使用Mark-Sweep,在空间不足以对从新生代中晋升过来的对象进行分配时才使用Mark-Compact。

    # 增量标记

    为了避免出现JavaScript应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的3种基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为全停顿。 在V8的分代式垃圾回收中,一次小垃圾回收只收集新生代,由于新生代默认配置得较小,且其中存活对象通常较少,所以即便它是全停顿的影响也不大。

    但V8的老生代通常配置得较大,且存活对象较多,全堆垃圾回收(full 垃圾回收)的标记、清理、整理等 动作造成的停顿就会比较可怕,需要设法改善。

    增量标记: 为了降低全堆垃圾回收带来的停顿时间,V8先从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记。 也就是拆分为许多小“步进”,每做完一“步进”就让JavaScript应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。

    # 查看垃圾回收日志

    查看垃圾回收日志的方式主要是在启动时添加--trace_gc参数。在进行垃圾回收时,将会从标准输出中打印垃圾回收的日志信息。

    node --trace_gc -e "var a = [];for (var i = 0; i < 1000000; i++) a.push(new Array(100));"
    
    [71982:0x118008000]       26 ms: Scavenge 2.4 (3.0) -> 2.0 (4.0) MB, 0.7 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
    [71982:0x118008000]       33 ms: Scavenge 2.6 (4.3) -> 2.5 (5.0) MB, 0.6 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
    [71982:0x118008000]       34 ms: Scavenge 3.2 (7.0) -> 3.2 (7.3) MB, 0.3 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
    [71982:0x118008000]       35 ms: Scavenge 4.5 (7.3) -> 4.5 (7.8) MB, 0.4 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
    [71982:0x118008000]       36 ms: Scavenge 5.1 (11.8) -> 4.9 (13.0) MB, 0.8 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
    [71982:0x118008000]       37 ms: Scavenge 8.3 (13.0) -> 8.6 (13.8) MB, 0.8 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
    [71982:0x118008000]       39 ms: Scavenge 8.8 (21.8) -> 8.4 (25.5) MB, 1.2 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
    [71982:0x118008000]       50 ms: Scavenge 16.3 (25.7) -> 17.1 (26.5) MB, 3.2 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
    [71982:0x118008000]       52 ms: Scavenge 17.1 (42.5) -> 16.1 (49.5) MB, 2.5 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
    [71982:0x118008000]       64 ms: Scavenge 32.7 (50.1) -> 34.3 (51.9) MB, 6.8 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
    [71982:0x118008000]       69 ms: Scavenge 34.3 (51.9) -> 32.3 (65.9) MB, 4.2 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
    [71982:0x118008000]       99 ms: Mark-sweep 86.4 (111.6) -> 85.0 (118.8) MB, 3.0 / 0.0 ms  (+ 8.1 ms in 127 steps since start of marking, biggest step 0.2 ms, walltime since start of marking 23 ms) (average mu = 1.000, current mu = 1.000) finalize incremental marking via stack guard GC in old space requested
    [71982:0x118008000]      437 ms: Scavenge 482.6 (517.2) -> 476.3 (511.0) MB, 35.8 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
    [71982:0x118008000]      495 ms: Mark-sweep 544.1 (579.1) -> 542.6 (577.6) MB, 6.1 / 0.0 ms  (+ 70.0 ms in 711 steps since start of marking, biggest step 0.4 ms, walltime since start of marking 235 ms) (average mu = 0.808, current mu = 0.808) finalize incremental marking via stack guard GC in old space requested
    

    V8提供了linux-tick-processor工具用于统计日志信息。该工具可以从Node源码的 deps/v8/tools目录下找到,Windows下的对应命令文件为windows-tick-processor.bat。将该目录添 加到环境变量PATH中,即可直接调用:

    linux-tick-processor v8.log

    # 02. 高效使用内存

    在V8面前,开发者所要具备的责任是如何让垃圾回收机制更高效地工作。

    # 作用域

    提到如何触发垃圾回收,第一个要介绍的是作用域(scope)。在JavaScript中能形成作用域 的有函数调用、with以及全局作用域.

    以如下代码为例:

    var foo = function () {
     var local = {};
    };
    

    foo()函数在每次被调用时会创建对应的作用域,函数执行结束后,该作用域将会销毁。同时作用域中声明的局部变量分配在该作用域上,随作用域的销毁而销毁。只被局部变量引用的对象存活周期较短。在这个示例中,由于对象非常小,将会分配在新生代中的From空间中。在作用域释放后,局部变量local失效,其引用的对象将会在下次垃圾回收时被释放。

    以上就是最基本的内存回收过程。

    1. 标识符查找 所谓标识符,可以理解为变量名

    在下面的代码中,执行 bar()函数时,将会遇到local变量:

    var bar = function () {
     console.log(local);
    };
    

    JavaScript在执行时会去查找该变量定义在哪里。它最先查找的是当前作用域,如果在当前作 用域中无法找到该变量的声明,将会向上级的作用域里查找,直到查到为止。

    1. 作用域链 在下面的代码中:
    var foo = function () {
      var local = "local var";
      var bar = function () {
        var local = "another var";
        var baz = function () {
          console.log(local);
        };
        baz();
      };
      bar();
    };
    foo();
    

    变量在作用域中的查找示意图

    1. 变量的主动释放 如果变量是全局变量(不通过var声明或定义在global变量上),由于全局作用域需要直到进程退出才能释放,此时将导致引用的对象常驻内存(常驻在老生代中)。如如果需要释放常驻内存的对象,可以通过delete操作来删除引用关系。或者将变量重新赋值,让旧的对象脱离引用关系。在接下来的老生代内存清除和整理的过程中,会被回收释放。 下面为示例代码:
    global.foo = "I am global object";
    console.log(global.foo); // => "I am global object"
    delete global.foo;
    // 或者重新赋值
    global.foo = undefined; // or null
    console.log(global.foo); // => undefined 
    

    提示

    是在V8中通过delete删除对象的属性有可能干扰V8的优化,所以通过赋值方式解除引用更好。

    # 闭包

    作用域链上的对象访问只能向上,这样外部无法向内部访问

    var foo = function () {
     var local = "局部变量";
     (function () {
     console.log(local);
     }());
    }; 
    
    输出:
    局部变量
    

    但在下面的代码中,却会得到local未定义的异常:

    var foo = function () {
     (function () {
     var local = "局部变量";
     }());
     console.log(local);
    }; 
    
    输出异常:
    console.log(local);
                  ^
    ReferenceError: local is not defined
    at foo (/Users/wsh/github/node-demo/index.js:68:15)
    at Object.<anonymous> (/Users/wsh/github/node-demo/index.js:70:1)
    

    在JavaScript中,实现外部作用域访问内部作用域中变量的方法叫做闭包。这得益于高阶函数的特性:函数可以作为参数或者返回值。示例代码的如下:

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

    一般而言,在bar()函数执行完成后,局部变量local将会随着作用域的销毁而被回收。注意返回值是一个匿名函数,且这个函数中具备了访问local的条件,虽然在后续的执行中,在外部作用域中还是无法直接访问local,但是若要访问它,只要通过这个中间函 数稍作周转即可。

    一旦有变量引用这个中间函数,这个中间函数将不会释放,同时也会使原始的作用域不会得到释放,作用域中产生的内存占用也不会得到释放。除非不再有引用,才会逐步释放。

    总结

    在正常的JavaScript执行中,无法立即回收的内存有闭包和全局变量引用这两种情况。由于V8的内存限制,要十分小心此类变量是否无限制地增加,因为它会导致老生代中的对象增多。

    # 03. 内存指标

    一般而言,应用中存在一些全局性的对象是正常的,而且在正常的使用中,变量都会自动释放回收。但是也会存在一些我们认为会回收但是却没有被回收的对象,这会导致内存占用无限增长。一旦增长达到V8的内存限制,将会得到内存溢出错误,进而导致进程退出。

    # 查看内存使用情况

    1. process.memoryUsage()可以查看内存使用情况
    2. os模块中的totalmem()和freemem()方法也可以查看内存使用情况
    # 查看进程的内存占用

    调用process.memoryUsage()可以看到Node进程的内存占用情况,示例代码如下:

    wsh@wsh ~ % node
    > process.memoryUsage()
    {
      rss: 118702080,
      heapTotal: 5005312,
      heapUsed: 3128976,
      external: 1672670,
      arrayBuffers: 9911
    }
    
    • rss是resident set size的缩写,即进程的常驻内存部分。进程的内存总共有几部分,一部分是rss,其余部分在交换区(swap)或者文件系统(filesystem)中。
    • heapTotal和heapUsed对应的是V8的堆内存信息。heapTotal是堆中总共申请的内存量,heapUsed表示目前堆中使用中的内存量。这3个值的单位都是字节。
    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("-----------------------------------------------------------");
    };
    showMem();
    
    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(); 
    

    输出结果:JavaScript 堆内存不足

    Process: heapTotal 4.27 MB heapUsed 2.59 MB rss 90.94 MB
    -----------------------------------------------------------
    Process: heapTotal 4.77 MB heapUsed 2.93 MB rss 94.16 MB
    -----------------------------------------------------------
    Process: heapTotal 164.82 MB heapUsed 162.94 MB rss 749.16 MB
    -----------------------------------------------------------
    Process: heapTotal 325.82 MB heapUsed 322.71 MB rss 1391.83 MB
    -----------------------------------------------------------
    Process: heapTotal 488.08 MB heapUsed 482.73 MB rss 2035.33 MB
    -----------------------------------------------------------
    Process: heapTotal 652.58 MB heapUsed 642.70 MB rss 2654.91 MB
    -----------------------------------------------------------
    Process: heapTotal 820.59 MB heapUsed 802.70 MB rss 3097.00 MB
    -----------------------------------------------------------
    Process: heapTotal 996.59 MB heapUsed 962.70 MB rss 3723.53 MB
    -----------------------------------------------------------
    Process: heapTotal 1156.59 MB heapUsed 1122.70 MB rss 4335.13 MB
    -----------------------------------------------------------
    Process: heapTotal 1316.60 MB heapUsed 1282.70 MB rss 4932.05 MB
    -----------------------------------------------------------
    Process: heapTotal 1476.60 MB heapUsed 1442.70 MB rss 5535.61 MB
    -----------------------------------------------------------
    Process: heapTotal 1636.61 MB heapUsed 1602.70 MB rss 6146.20 MB
    -----------------------------------------------------------
    Process: heapTotal 1796.61 MB heapUsed 1762.70 MB rss 6786.23 MB
    -----------------------------------------------------------
    Process: heapTotal 1956.61 MB heapUsed 1922.70 MB rss 7426.31 MB
    -----------------------------------------------------------
    Process: heapTotal 2115.87 MB heapUsed 2082.05 MB rss 8099.53 MB
    -----------------------------------------------------------
    
    <--- Last few GCs --->
    
    [75211:0x118008000]     1433 ms: Scavenge 1762.6 (1796.6) -> 1762.6 (1796.6) MB, 11.7 / 0.0 ms  (average mu = 1.000, current mu = 1.000) allocation failure 
    [75211:0x118008000]     1638 ms: Mark-sweep 1922.6 (1956.6) -> 1921.9 (1955.9) MB, 110.0 / 0.1 ms  (+ 35.4 ms in 10 steps since start of marking, biggest step 6.0 ms, walltime since start of marking 1348 ms) (average mu = 1.000, current mu = 1.000) alloca
    
    <--- JS stacktrace --->
    
    FATAL ERROR: MarkCompactCollector: young object promotion failed Allocation failed - JavaScript heap out of memory
    
    # 查看系统的内存占用

    os模块中的totalmem()和freemem()这两个方法用于查看操作系统的内存使用情况,它们分别返回系统的总内存和闲置内存,以字节为单位.

    % node
    > os.totalmem()
    8589934592
    > os.freemem() 
    110432256
    > 
    

    从输出信息可以看到我的电脑的总内存为8 GB,当前闲置内存大致为1GB。

    # 堆外内存

    通过process.momoryUsage()的结果可以看到,堆中的内存用量总是小于进程的常驻内存用量,这意味着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;
    }; 
    

    唯一变化大的是rss的值,原因是Buffer对象不同于其他对象,它不经过V8的内存分配机制,所以也不会有堆内存的大小限制。

    总结

    Node的内存构成主要由通过V8进行分配的部分和Node自行分配的部分。受V8的垃圾回收限制的主要是V8的堆内存

    # 04. 内存泄漏

    内存泄漏实质

    是应当回收的对象出现意外而没有被回收,变成了常驻在老生代中的对象

    通常,造成内存泄漏的原因有如下几个。

    • 缓存。
    • 队列消费不及时。
    • 作用域未释放。

    # 慎将内存当做缓存

    严格意义的缓存有着完善的过期策略,而普通对象的键值对并没有。 如果需要,只要限定缓存对象的大小,加上完善的过期策略以防止内存无限制增长,还是可以一用的。 如下代码虽然利用JavaScript对象十分容易创建一个缓存对象,但是受垃圾回收机制的影响, 只能小量使用:

    var cache = {};
    var get = function (key) {
     if (cache[key]) {
     return cache[key];
     } else {
     // get from otherwise
     }
    };
    var set = function (key, value) {
     cache[key] = value;
    };
    
    1. 缓存限制策略 为了解决缓存中的对象永远无法释放的问题,需要加入一种策略来限制缓存的无限增长。 模块limitablemap:记录键在数组中,一旦超过数量,就以先进先出的方式进行淘汰
    var LimitableMap = function (limit) {
     this.limit = limit || 10;
     this.map = {};
     this.keys = [];
    };
    var hasOwnProperty = Object.prototype.hasOwnProperty;
    LimitableMap.prototype.set = function (key, value) {
     var map = this.map;
     var keys = this.keys;
     if (!hasOwnProperty.call(map, key)) {
     if (keys.length === this.limit) {
     var firstKey = keys.shift();
     delete map[firstKey];
     }
     keys.push(key);
     }
     map[key] = value;
    };
    LimitableMap.prototype.get = function (key) {
     return this.map[key];
    };
    module.exports = LimitableMap; 
    

    由于通过exports导出的函数,可以访问文件模块中的私有变量,这样每个文件模块在编译执行后形成的作用域因为模块缓存的原因,不会被释放。由于模块的缓存机制,模块是常驻老生代的 示例代码如下所示:

    (function (exports, require, module, __filename, __dirname) {
     var local = "局部变量";
     exports.get = function () {
     return local;
     };
    }); 
    
    1. 缓存的解决方案

    进程之间无法共享内存。如果在进程内使用缓存,这些缓存不可避免地有重复,对物理内存的使用是一种浪费。

    如何使用大量缓存,目前比较好的解决方案是采用进程外的缓存,进程自身不存储状态。外部的缓存软件有着良好的缓存过期淘汰策略以及自有的内存管理,不影响Node进程的性能。它的好处多多,在Node中主要可以解决以下两个问题。

    • 将缓存转移到外部,减少常驻内存的对象的数量,让垃圾回收更高效。
    • 进程之间可以共享缓存。

    Redis (opens new window) Memcached (opens new window)

    # 关注队列状态

    在大多数应用场景下,消费的速度远远大于生产的速度,内存泄漏不易产生。但是一旦消费速度低于生产速度, 将会形成堆积。

    举个实际的例子,有的应用会收集日志。如果欠缺考虑,也许会采用数据库来记录日志。日志通常会是海量的,数据库构建在文件系统之上,写入效率远远低于文件直接写入,于是会形成数据库写入操作的堆积,而JavaScript中相关的作用域也不会得到释放,内存占用不会回落,从而出现内存泄漏。

    的解决方案: 任意异步调用都应该包含超时机制,一旦在限定的时间内未完成响应,通过回调函数传递超时异常,使得任意异步调用的回调都具备可控的响应时间,给消费速度一个下限值。

    # 05. 内存泄漏排查

    在Node中,由于V8的堆内存大小的限制,它对内存泄漏非常敏感。当在线服务的请求量变大时,哪怕是一个字节的泄漏都会导致内存占用过高。 现在已经有许多工具用于定位Node应用的内存泄漏,下面是一些常见的工具。

    • node-heapdump。这是Node核心贡献者之一Ben Noordhuis编写的模块,它允许对V8堆内 存抓取快照,用于事后分析。
    • node-mtrace。由Jimb Esser提供,它使用了GCC的mtrace工具来分析堆的使用。
    • dtrace。在Joyent的SmartOS系统上,有完善的dtrace工具用来分析内存泄漏。
    • node-memwatch。来自Mozilla的Lloyd Hilaiel贡献的模块,采用WTFPL许可发布。

    # node-heapdump

    // 1. 安装node-heapdump
    npm install heapdump 
    
    // 2. 引入
    var heapdump = require('heapdump');
    

    在Chrome的开发者工具中选中Profiles面板,右击该文件后,从弹出的快捷菜单中选择Load... 选项,打开刚才的快照文件,就可以查看堆内存中的详细信息.

    # 06. 大内存应用

    在Node提供了stream模块用于处理大文件。stream模块是Node的原生模块,直接引用即可。stream继承自EventEmitter,具备基本的自定义事件功能,同时抽象出标准的事件和方法。它分可读和可写两种。

    • fs的createReadStream()和createWriteStream()方法可以分别用于创建文件的可读流和可写流。
    • process模块中的stdin和stdout则分别是可读流和可写流的示例

    由于V8的内存限制,我们无法通过fs.readFile()和fs.writeFile()直接进行大文件的操作,而改用fs.createReadStream()和fs.createWriteStream()方法通过流的方式实现对大文件的操作。

    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();
    }); 
    

    由于读写模型固定,上述方法有更简洁的方式,具体如下所示:

    var reader = fs.createReadStream('in.txt');
    var writer = fs.createWriteStream('out.txt');
    reader.pipe(writer);
    

    可读流提供了管道方法pipe(),封装了data事件和写入操作。通过流的方式,上述代码不会受到V8内存限制的影响,有效地提高了程序的健壮性。

    提示

    如果不需要进行字符串层面的操作,则不需要借助V8来处理,可以尝试进行纯粹的Buffer操 作,这不会受到V8堆内存的限制。但是这种大片使用内存的情况依然要小心,即使V8不限制堆 内存的大小,物理内存依然有限制。

    #node
    异步编程
    理解Buffer

    ← 异步编程 理解Buffer→

    最近更新
    01
    组件通信方式
    01-07
    02
    UIAbility
    01-07
    03
    ATKTS
    01-06
    更多文章>
    Theme by Vdoing | Copyright © 2022-2025 Wsh | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式