ホームページ  >  記事  >  ウェブフロントエンド  >  Nodeのメモリ制御に関する記事

Nodeのメモリ制御に関する記事

青灯夜游
青灯夜游転載
2023-04-26 17:37:052211ブラウズ

ノンブロッキングおよびイベント駆動に基づいて構築されたノード サービスには、メモリ消費量が少ないという利点があり、大量のネットワーク リクエストの処理に非常に適しています。大量のリクエストを前提として、「メモリ制御」に関する問題を考慮する必要があります。

Nodeのメモリ制御に関する記事

1. V8 のガベージ コレクション メカニズムとメモリ制限

Js はガベージ コレクション メカニズムを使用して自動メモリ管理を実行するため、開発者がそれを行う必要はありません。他の言語 ( c/c ) と同様に、コードを記述するプロセス中はメモリの割り当てと解放に常に注意してください。ブラウザーでは、ガベージ コレクション メカニズムはアプリケーションのパフォーマンスにほとんど影響を与えませんが、パフォーマンスが重視されるサーバー側プログラムの場合、メモリ管理の品質とガベージ コレクションの品質がサービスに影響を与えます。 [関連チュートリアルの推奨事項: nodejs ビデオ チュートリアル プログラミング教育 ]

1.1 ノードと V8

Node は、Chrome 上に構築された Js ランタイムです。 V8 は Node

の Js スクリプト エンジンです。1.2 V8

のメモリ制限一般的なバックエンド言語では、基本的なメモリ使用量に制限はありませんが、Node When Js がメモリを使用する場合は、 、メモリの一部しか使用できません。このような制限の下では、Node は大きなメモリ オブジェクトを直接操作できません。

この問題の主な原因は、Node が V8 をベースに構築されているためで、Node で使用される Js オブジェクトは基本的に V8 独自の方法で割り当ておよび管理されます。

1.3 V8 オブジェクトの割り当て

V8 では、すべての Js オブジェクトはヒープを通じて割り当てられます。

v8 でのメモリ使用量の確認

截屏2023-01-21 下午3.35.30.png

heapTotal と heap Used は V8 のヒープ メモリ使用量で、前者は適用されているヒープ メモリ、後者は現在使用されている量です。

コード内で変数が宣言され、値が割り当てられると、使用されるオブジェクトのメモリがヒープ上に割り当てられます。適用されたヒープ空きメモリが新しいオブジェクトを割り当てるのに十分でない場合、ヒープ サイズが V8

の制限を超えるまでヒープ メモリは適用され続けます。V8 がヒープ サイズを制限するのはなぜですか? 表面的な理由は次のとおりです。 V8 は元々ブラウザだったということです。設計に関して言えば、大量のメモリを使用するシナリオに遭遇する可能性は低いです。 Web ページの場合、V8 の制限値で十分です。根本的な理由は、V8 のガベージ コレクション メカニズムの制限です。公式声明によると、1.5G のガベージ コレクション ヒープ メモリを例にとると、v8 では小規模なガベージ コレクションの実行に 50 ミリ秒以上かかり、非増分ガベージ コレクションの実行にも 1 秒以上かかります。これは、ガベージ コレクション中に JS スレッドの実行が一時停止され、アプリケーションのパフォーマンスと応答性が急激に低下する時間です。現在の考慮事項では、ヒープ メモリを直接制限することが適切な選択です。

この制限は緩和できます。ノードの起動時に、--max-old-space-size または --max-new-space-size ## を渡すことができます。 #メモリ制限のサイズを調整する場合、一度有効になると動的に変更することはできません。例:

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

1.4 V8 のガベージ コレクション メカニズム

v8 で使用されるさまざまなガベージ コレクション アルゴリズム

1.4.1 V8 の主要なガベージ コレクション アルゴリズム

v8 のガベージコレクション戦略は主に世代別ガベージ コレクション メカニズムに基づいています。

実際のアプリケーションでは、オブジェクトのライフサイクルは異なります。現在のガベージ コレクション アルゴリズムでは、オブジェクトの生存時間に応じて異なる世代でメモリ ガベージ コレクションが実行され、異なる世代のメモリに対してより効率的なアルゴリズムが適用されます。

  • V8 メモリの世代

    v8 では、メモリは主に新世代と旧世代の 2 つの世代に分かれています。若い世代のオブジェクトは生存時間が短いオブジェクトであり、古い世代のオブジェクトは生存時間が長いオブジェクト、またはメモリに常駐しているオブジェクトです。

    新世代のメモリ空間旧世代のメモリ空間

    v8 ヒープの全体サイズ = 新しい世代が使用するメモリ領域と古い世代が使用するメモリ領域

    v8 ヒープ メモリの最大値は、約 1.4GB のメモリしか使用できません64 ビット システムでは約 1.4 GB、32 ビット システムでは約 1.4 GB のみ。約 0.7 GB のメモリを使用可能

  • スカベンジ アルゴリズム

    世代、オブジェクトに基づいて新世代では、主に Scavenge アルゴリズムを通じてガベージ コレクションが行われます。 Scavenge の具体的な実装では、主に Cheney アルゴリズムが使用されます。 Cheney のアルゴリズムは、コピーによって実装されたガベージ コレクション アルゴリズムです。ヒープ メモリを 2 つの部分に分割し、空間の各部分をセミスペースと呼びます。 2 つのセミスペースのうち、1 つだけが使用中で、もう 1 つはアイドル状態です。使用中の半空間空間をFrom空間、アイドル状態の空間をTo空間と呼びます。オブジェクトを割り当てるときは、最初に From スペースに割り当てられます。ガベージ コレクションが開始されると、From スペースに残っているオブジェクトがチェックされ、これらの生き残ったオブジェクトが To スペースにコピーされ、生き残っていないオブジェクトによって占められていたスペースが解放されます。コピーが完了すると、From スペースと To スペースの役割が逆転します。つまり、ガベージ コレクション プロセス中に、生き残ったオブジェクトが 2 つのセミスペース間でコピーされます。 Scavenge の欠点は、パーティション分割スペースとコピー メカニズムによって決まるヒープ メモリの半分しか使用できないことです。ただし、Scavenge は存続するオブジェクトのみをコピーし、ライフ サイクルが短いシナリオでは存続するオブジェクトのごく一部のみが使用されるため、時間効率の点で優れたパフォーマンスを発揮します。 Scavenge は時間のために空間を犠牲にする典型的なアルゴリズムであるため、大規模なガベージ コレクションすべてに適用することはできません。しかし、新世代のオブジェクトのライフサイクルは短く、このアルゴリズムに適しているため、Scavenge は新世代のアプリケーションに非常に適しています。 Nodeのメモリ制御に関する記事実際に使用されるヒープ メモリは、新しい世代の 2 つの半空白スペースと古い世代で使用されたメモリ サイズの合計です。

  • オブジェクトが複数のコピー後も存続する場合、そのオブジェクトはライフサイクルが長いオブジェクトとみなされ、古い世代に移動され、新しいアルゴリズムを使用して管理されます。オブジェクトを若い世代から古い世代に移動するプロセスは、プロモーションと呼ばれます。

    単純なスカベンジ プロセスでは、From スペースに残っているオブジェクトが To スペースにコピーされ、From スペースと To スペースの役割が逆転します (反転)。ただし、世代別ガベージ コレクションの前提では、From スペースに残っているオブジェクトは To スペースにコピーされる前にチェックされる必要があります。特定の条件下では、生存期間が長いオブジェクトを古い世代に移動する必要があります。つまり、オブジェクトの昇格が完了します。

    オブジェクトの昇格には主に 2 つの条件があり、1 つはオブジェクトが Scavenge リサイクルを経験しているかどうか、もう 1 つは To 領域のメモリ使用率が制限を超えていることです。

    デフォルトでは、V8 のオブジェクト割り当ては主に From スペースに集中しています。オブジェクトが From スペースから To スペースにコピーされると、そのメモリ アドレスがチェックされて、オブジェクトが Scavenge リサイクルを経験したかどうかが判断されます。経験済みであればFrom空間から旧世代空間にオブジェクトがコピーされ、経験していなければTo空間にコピーされます。プロモーションのフローチャートは以下のとおりです。

    未命名文件 (1).png

    #もう一つの判定条件は、To 領域のメモリ使用率です。 From スペースから To スペースにオブジェクトをコピーするとき、To スペースが 25% 使用されている場合、オブジェクトは古い世代スペースに直接プロモートされます。プロモート フローチャートは次のとおりです:

    未命名文件 (2).png

    25% 制限を設定する理由: この Scavenge のリサイクルが完了すると、この To 空間は From 空間になり、次のメモリ割り当てはこの空間で実行されます。この比率が高すぎると、その後のメモリ割り当てに影響します。

    オブジェクトは昇格後、古い世代空間で生存期間の長いオブジェクトとして扱われ、新しいリサイクル アルゴリズムによって処理されます。

    • マーク スイープとマーク コンパクト

      旧世代のオブジェクトの場合、生き残ったオブジェクトが大きな割合を占めるため、Scavenge を使用すると次の 2 つの問題があります。 1 つは、生き残るオブジェクトが多く、生き残るオブジェクトのコピー効率が非常に低いということ、もう 1 つはスペースの半分が無駄になるということです。この目的を達成するために、v8 では主に、旧世代のガベージ コレクションに Mark-Sweet と Mark-Compact の組み合わせを使用します。

      マークスイープとはマーククリアのことで、マークとクリアの2段階に分かれます。 Scavenge と比較して、Mark-Sweet はメモリ空間を 2 つに分割しないため、半分の空間を無駄にする動作はありません。ライブ オブジェクトをコピーする Scavenge とは異なり、Mark-Sweet はマーキング フェーズ中にヒープ内のすべてのオブジェクトを走査し、ライブ オブジェクトにマークを付けます。その後のクリア フェーズでは、マークされていないオブジェクトのみがクリアされます。 Scavenge は生きているオブジェクトのみをコピーするのに対し、Mark-Sweat は死んだオブジェクトのみをクリーンアップすることがわかります。新世代では生きている物がわずかであり、古い世代では死んだ物がわずかであるため、2 つのリサイクル方法で効率的に処理できます。旧世代空間でマークスイープをマークした後の模式図は以下の通りです 黒い部分がデッドオブジェクトとしてマークされています

    未命名文件 (4).png

    マークスイープの最大の問題は、スペースをマークしてクリアした後、メモリスペースが不連続になってしまうことです。この種のメモリの断片化は、その後のメモリ割り当てに問題を引き起こします。大きなオブジェクトを割り当てる必要がある場合、断片化されたすべての領域では割り当てを完了できず、ガベージ コレクションが事前にトリガーされるため、このリサイクルは不要です。

    Mark-Compact は、Mark-Sweet のメモリ断片化の問題を解決します。 Mark-Compact は、Mark-Sweet から進化したマーク コンパイルを意味します。違いは、オブジェクトが死んだものとしてマークされた後、クリーニング プロセス中に生きているオブジェクトが一方の端に移動され、移動が完了した後、境界の外側のメモリが直接クリアされることです。生きているオブジェクトをマークして移動した後の概略図。白いグリッドは生きているオブジェクト、暗いグリッドは死んだオブジェクト、明るいグリッドは生きているオブジェクトを移動した後に残った穴です。

    未命名文件 (5).png

    移動の完了後、右端の生き残ったオブジェクトの背後にあるメモリ領域を直接クリアして、リサイクルを完了できます。

    V8 のリサイクル戦略では、Mark-Sweet と Mark-Compact が組み合わせて使用​​されます。

    3 つの主要なガベージ コレクション アルゴリズムの簡単な比較

    リサイクル アルゴリズム Mark-Sweet Mark-Compact スカベンジ
    速度 最も遅い 最速
    スペース オーバーヘッド 少ない (断片化あり) 少ない (断片化なし) ダブル スペース (断片化なし)
    オブジェクトを移動するかどうか No Yes Yes

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

    • Incremental Marking

      为了避免出现Js应用逻辑与垃圾回收器看到的不一致情况,垃圾回收的3种基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为称为“全停顿”(stop-the-world).在v8的分代式垃圾回收中,一次小垃圾回收只收集新生代,由于新生代默认配置得较小,且其中存活对象通常较少,所以即便它是全停顿的影响也不大。但v8的老生代通常配置得较大,且存活对象较多,全堆垃圾回收(full垃圾回收)的标记、清理、整理等动作造成的停顿就会比较可怕,需要设法改善

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

    Nodeのメモリ制御に関する記事

    v8在经过增量标记的改进后,垃圾回收的最大停顿时间可以减少到原本的1/6左右。 v8后续还引入了延迟清理(lazy sweeping)与增量式整理(incremental compaction),让清理与整理动作也变成增量式的。同时还计划引入标记与并行清理,进一步利用多核性能降低每次停顿的时间。

    1.5 查看垃圾回收的日志

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

    node --trace_gc -e "var a = [];for (var  i = 0; i < 1000000; i++) a.push(new Array(100));" > gc.log

    截屏2023-01-22 下午7.35.39.png

    在Node启动时使用--prof参数,可以得到v8执行时的性能分析数据,包含了垃圾回收执行时占用的时间。以下面的代码为例

    // test.js
    for (var i = 0; i < 1000000; i++) {
        var a = {};
    }
    
    node --prof test.js

    会生成一个v8.log日志文件

    2. 高效使用内存

    如何让垃圾回收机制更高效地工作

    2.1 作用域(scope)

    在js中能形成作用域的有函数调用、with以及全局作用域

    如下代码:

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

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

    2.1.1 标识符查找

    标识符,可以理解为变量名。下面的代码,执行bar()函数时,将会遇到local变量

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

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

    2.1.2作用域链

    在下面的代码中

    var foo = function(){
        var local = &#39;local var&#39;;
        var bar = function(){
            var local = &#39;another var&#39;;
            var baz = function(){
                console.log(local)
            };
            baz()
        }
        bar()
    }
    foo()

    baz()函数中访问local变量时,由于作用域中的变量列表中没有local,所以会向上一个作用域中查找,接着会在bar()函数执行得到的变量列表中找到了一个local变量的定义,于是使用它。尽管在再上一层的作用域中也存在local的定义,但是不会继续查找了。如果查找一个不存在的变量,将会一直沿着作用域链查找到全局作用域,最后抛出未定义错误。

    2.1.3 变量的主动释放

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

    global.foo = "I am global object"
    console.log(global.foo);// => "I am global object"
    delete global.foo;
    // 或者重新赋值
    global.foo = undefined;
    console.log(global.foo); // => undefined

    虽然delete操作和重新赋值具有相同的效果,但是在V8中通过delete删除对象的属性有可能干扰v8的优化,所以通过赋值方式解除引用更好。

    2.2 闭包

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

    js实现外部作用域访问内部作用域中变量的方法叫做闭包。得益于高阶函数的特性:函数可以作为参数或者返回值。

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

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

    闭包是js的高级特性,利用它可以产生很多巧妙的效果。它的问题在于,一旦有变量引用这个中间函数,这个中间函数将不会释放,同时也会使原始的作用域不会得到释放,作用域中产生的内存占用也不会得到释放。

    无法立即回收的内存有闭包和全局变量引用这两种情况。由于v8的内存限制,要注意此变量是否无限制地增加,会导致老生代中的对象增多。

    3.内存指标

    会存在一些认为会回收但是却没有被回收的对象,会导致内存占用无限增长。一旦增长达到v8的内存限制,将会得到内存溢出错误,进而导致进程退出。

    3.1 查看内存使用情况

    process.memoryUsage()可以查看内存使用情况。除此之外,os模块中的totalmem()和freemem()方法也可以查看内存使用情况

    3.1.1 查看进程的内存使用

    调用process.memoryUsage()可以看到Node进程的内存占用情况

    截屏2023-01-21 下午3.35.30.png

    rss是resident set size的缩写,即进程的常驻内存部分。进程的内存总共有几部分,一部分是rss,其余部分在交换区(swap)或者文件系统(filesystem)中。

    除了rss外,heapTotal和heapUsed对应的是v8的堆内存信息。heapTotal是堆中总共申请的内存量,heapUsed表示目前堆中使用中的内存量。单位都是字节。示例如下:

    var showMem = function () {
      var mem = process.memoryUsage()
      var format = function (bytes) {
        return (bytes / 1024 / 1024).toFixed(2) + &#39;MB&#39;;
      }
      console.log(&#39;Process: heapTotal &#39; + format(mem.heapTotal) +
        &#39; heapUsed &#39; + format(mem.heapUsed) + &#39; rss &#39; + format(mem.rss))
      console.log(&#39;---------------------&#39;)
    }
    
    var useMem = function () {
      var size = 50 * 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();

    截屏2023-01-23 下午12.52.43.png

    在内存达到最大限制值的时候,无法继续分配内存,然后进程内存溢出了。

    3.1.2 查看系统的内存占用

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

    截屏2023-01-23 下午1.10.54.png

    3.2 堆外内存

    通过process.memoryUsage()的结果可以看到,堆中的内存用量总是小于进程的常驻内存用量,意味着Node中的内存使用并非都是通过v8进行分配的。将那些不是通过v8分配的内存称为堆外内存

    将上面的代码里的Array变为Buffer,将size变大

    var useMem = function () {
      var size = 200 * 1024 * 1024;
      var buffer = Buffer.alloc(size); // new Buffer(size)是旧语法
      for (var i = 0; i < size; i++) {
        buffer[i] = 0
      }
      return buffer
    }

    输出结果如下:

    截屏2023-01-23 下午1.29.00.png

    内存没有溢出,改造后的输出结果中,heapTotal与heapUsed的变化极小,唯一变化的是rss的值,并且该值已经远远超过v8的限制值。原因是Buffer对象不同于其它对象,它不经过v8的内存分配机制,所以也不会有堆内存的大小限制。意味着利用堆外内存可以突破内存限制的问题

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

    4. 内存泄漏

    Node对内存泄漏十分敏感,内存泄漏造成的堆积,垃圾回收过程中会耗费更多的时间进行对象扫描,应用响应缓慢,直到进程内存溢出,应用崩溃。

    在v8的垃圾回收机制下,大部分情况是不会出现内存泄漏的,但是内存泄漏通常产生于无意间,排查困难。内存泄漏的情况不尽相同,但本质只有一个,那就是应当回收的对象出现意外而没有被回收,变成了常驻在老生代中的对象。通常原因有如下几个:

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

    4.1 慎将内存当缓存用

    缓存在应用中的作用十分重要,可以十分有效地节省资源。因为它的访问效率要比 I/O 的效率高,一旦命中缓存,就可以节省一次 I/O时间。

    对象被当作缓存来使用,意味着将会常驻在老生代中。缓存中存储的键越多,长期存活的对象也就越多,导致垃圾回收在进行扫描和整理时,对这些对象做无用功。

    Js开发者喜欢用对象的键值对来缓存东西,但这与严格意义上的缓存又有着区别,严格意义的缓存有着完善的过期策略,而普通对象的键值对并没有。是一种以内存空间换CPU执行时间。示例代码如下:

    var cache = {}; 
    var get = function (key) { 
     if (cache[key]) { 
         return cache[key]; 
     } else { 
     // get from otherwise 
     } 
    }; 
    var set = function (key, value) { 
     cache[key] = value; 
    };

    所以在Node中,拿内存当缓存的行为应当被限制。当然,这种限制并不是不允许使用,而是要小心为之。

    4.1.1 缓存限制策略

    为了解决缓存中的对象永远无法释放的问题,需要加入一种策略来限制缓存的无限增长。可以实现对键值数量的限制。下面是其实现:

    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;

    记录键在数组中,一旦超过数量,就以先进先出的方式进行淘汰。

    4.1.2 缓存的解决方案

    直接将内存作为缓存的方案要十分慎重。除了限制缓存的大小外,另外要考虑的事情是,进程之间无法共享内存。如果在进程内使用缓存,这些缓存不可避免地有重复,对物理内存的使用是一种浪费。

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

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

    目前,市面上较好的缓存有Redis和Memcached。

    4.2 关注队列状态

    队列在消费者-生产者模型中经常充当中间产物。这是一个容易忽略的情况,因为在大多数应用场景下,消费的速度远远大于生产的速度,内存泄漏不易产生。但是一旦消费速度低于生产速度,将会形成堆积, 导致Js中相关的作用域不会得到释放,内存占用不会回落,从而出现内存泄漏。

    解决方案应该是监控队列的长度,一旦堆积,应当通过监控系统产生报警并通知相关人员。另一个解决方案是任意异步调用都应该包含超时机制,一旦在限定的时间内未完成响应,通过回调函数传递超时异常,使得任意异步调用的回调都具备可控的响应时间,给消费速度一个下限值。

    5. 内存泄漏排查

    常见的工具

    • v8-profiler: 可以用于对V8堆内存抓取快照和对CPU进行分析
    • node-heapdump: 允许对V8堆内存抓取快照,用于事后分析
    • node-mtrace: 使用了GCC的mtrace工具来分析堆的使用
    • dtrace:有完善的dtrace工具用来分析内存泄漏
    • node-memwatch

    6. 大内存应用

    由于Node的内存限制,操作大文件也需要小心,好在Node提供了stream模块用于处理大文件。

    stream模块是Node的原生模块,直接引用即可。stream继承自EventEmitter,具备基本的自定义事件功能,同时抽象出标准的事件和方法。它分可读和可写两种。Node中的大多数模块都有stream的应用,比如fs的createReadStream()和createWriteStream()方法可以分别用于创建文件的可读流和可写流,process模块中的stdin和stdout则分别是可读流和可写流的示例。

    由于V8的内存限制,我们无法通过fs.readFile()和fs.writeFile()直接进行大文件的操作,而改用fs.createReadStream()和fs.createWriteStream()方法通过流的方式实现对大文件的操作。下面的代码展示了如何读取一个文件,然后将数据写入到另一个文件的过程:

    var reader = fs.createReadStream(&#39;in.txt&#39;);
    var writer = fs.createWriteStream(&#39;out.txt&#39;);
    reader.on(&#39;data&#39;, function (chunk) {
      writer.write(chunk);
    });
    reader.on(&#39;end&#39;, function () {
      writer.end();
    });
    
    // 简洁的方式
    var reader = fs.createReadStream(&#39;in.txt&#39;); 
    var writer = fs.createWriteStream(&#39;out.txt&#39;); 
    reader.pipe(writer);

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

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

    更多node相关知识,请访问:nodejs 教程

以上がNodeのメモリ制御に関する記事の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。