Node內存限制與垃圾回收

對象分配

全部的JS對象都是經過堆來進行分配的。html

使用process.memoryUsage()查看使用狀況 Node.js v6.11.3文檔
heapTotal and heapUsed refer to V8's memory usage. external refers to the memory usage of C++ objects bound to JavaScript objects managed by V8.node

> process.memoryUsage()
{ rss: 27541504, 
  heapTotal: 9437184, 
  heapUsed: 5897048,
  external: 8935 }
  // 單位 字節
  // rss (resident set size) 常駐進程內存 全部內存佔用
  // heapTotal 已申請堆內存
  // heapUsed 已使用堆內存
  // external c++對象綁定到js的內存
複製代碼

內存限制

內存限制主要緣由是v8的垃圾回收制度。1.5GB內存作一次小的回收須要50MS,作一次非增量性回收須要1S以上,而且這會使JS線程暫停。所以限制內存。c++

img

V8的堆組成

V8的堆由一系列區域組成:算法

新生代區:大多數對象的建立被分配在這裏,這個區域很小,但垃圾回收很是頻繁,獨立於其它區。api

老生代指針區:包含大部分可能含有指向其它對象指針的對象。大多數重新生代晉升(存活一段時間)的對象會被移動到這裏。數組

老生代數據區:包含原始數據對象(沒有指針指向其它對象)。Strings、boxed numbers以及雙精度unboxed數組重新生代中晉升後被移到這裏。緩存

大對象區:這裏存放大小超過其它區的大對象。每一個對象都有本身mmap內存。大對象不會被回收。bash

代碼區:代碼對象(即包含被JIT處理後的指令對象)存放在此。惟一的有執行權限的區域(代碼過大也可能存放在大對象區,此時它們也可被執行,但不表明大對象區都有執行權限)。閉包

Cell區、屬性Cell區以及map區:包含cell、屬性cell以及map。每一個區都存放他們指向的相同大小以及相同結構的對象。ide

v8在64位系統下只能使用1.4GB內存,在32位系統下只能使用0.7GB內存。

如何解除內存限制?

利用堆外內存: 使用Buffer類。Buffer 性能相關部分由C++實現。Buffer教程

在啓動node時,傳遞--max-old-space-size=4096 (調整老生代內存限制,單位爲mb。--max-new-space-size 已經不可用了)

使用stream處理大文件 stream教程

官方建議:it is recommended that you split your single process into several workers if you are hitting memory limits. (拆分進程)

垃圾回收機制

img

V8的垃圾回收有以下幾個特色
當處理一個垃圾回收週期時,暫停全部程序的執行。(stop-the-world 全停頓)  

在大多數垃圾回收週期,每次僅處理部分堆中的對象,使暫停程序所帶來的影響降至最低。(增量標記等算法) 

準確知道在內存中全部的對象及指針,避免錯誤地把對象當成指針所帶來的內存泄露。(標記指針法:在每一個指針的末位預留一位來標記這個字表明的是指針或數據。)

在V8中,對象堆被分爲兩個部分:新建立的對象所在的新生代,以及在一個垃圾回收週期後存活的對象被提高到的老生代。
若是一個對象在一個垃圾回收週期中被移動,那麼V8將會更新全部指向此對象的指針。
複製代碼

沒有一種算法可以勝任全部場景,所以現代垃圾回收算法中按對象的存活時間將內存的垃圾回收進行不一樣的分代,而後分別對不一樣分代實行不一樣算法

v8將內存分爲新生代和老生代

主要算法
  1. Scavenge算法 (打掃)

    特色:犧牲空間換時間

    將堆內存一分爲二,一個處於使用中,一個處於閒置。
    檢查使用中空間的存活對象,將存活對象複製到閒置空間中。
    完成複製後閒置空間和使用中空間角色互換。
    當一個對象通過屢次複製依然存活時,它會被認爲是生命週期較長的對象,會被晉升到老生代中。
    晉升的條件有兩個,一個是對象是否經歷過Scavenge回收,一個是空閒空間的內存佔用比超過25%。(爲什麼不是50%呢?)

    缺點:只能使用一半的堆空間,適合應用在新生代中。

  2. Mark-Sweep & Mark-Compact (標記清除 & 標記整理)

    Mark-Sweep遍歷堆中的全部對象,標記活着的對象。
    在清除階段清除全部沒有被標記的對象。
    缺點在於內存空間會出現不連續的狀態。

    Mark-Compact與Mark-Sweep的差異在於,在整理過程當中,將活着的對象往一端移動,而後清理掉邊界外的內存。

速度: Scavenge>Mark-Sweep>Mark-Compact

  1. Incremental Marking (增量標記)

    前三種算法都須要將應用邏輯暫停,待垃圾回收完成後再恢復,即「全停頓」。

    新生代較小,全停頓影響不大。但老生代很大,全停頓影響很大。

    爲下降影響,將標記階段改成增量標記,也就是將標記拆分爲不少小標記,每作完一步就讓js邏輯執行一下子,垃圾回收與JS邏輯交替執行。

  2. lazy sweeping & incremental compaction 等

內存泄漏

常見緣由
1.緩存
2.隊列消費不及時    
3.做用域未釋放複製代碼

case1 : 緩存

var cache = {};
function set (key, value){
    cache[key] = value;
}
複製代碼

case2 : 無限增加數組

var arr = [];
function x (value){
    arr.push(value);
}
複製代碼

case3: 無限重連致使的內存泄漏

const net = require('net');
let client = new net.Socket();

function connect() {
    client.connect(26665, '127.0.0.1', function callbackListener() {
    console.log('connected!');
});
}

//第一次鏈接
connect();

client.on('error', function (error) {
    // console.error(error.message);
});

client.on('close', function () {
    //console.error('closed!');
    //泄漏代碼
    client.destroy();
    setTimeout(connect, 1);
});
複製代碼

泄漏產生的緣由其實也很簡單:event.js 核心模塊實現的事件發佈/訂閱本質上是一個js對象結構(在v6版本中爲了性能採用了new EventHandles(),而且把EventHandles的原型置爲null來節省原型鏈查找的消耗),所以咱們每一次調用 event.on 或者 event.once 至關於在這個對象結構中對應的 type 跟着的數組增長一個回調處理函數。

那麼這個例子裏面的泄漏屬於很是隱蔽的一種:net 模塊的重連每一次都會給 client 增長一個 connect事件 的偵聽器,若是一直重連不上,偵聽器會無限增長,從而致使泄漏。

case4: 小測試

泄漏了嗎?
var run = function () {
  var str = new Array(1000000).join('*');
  var doSomethingWithStr = function () {
    if (str === 'something')
      console.log("str was something");
  };
  doSomethingWithStr();
};
setInterval(run, 1000);
複製代碼
泄漏了嗎?
var run = function () {
  var str = new Array(1000000).join('*');
  var logIt = function () {
    console.log('interval');
  };
  setInterval(logIt, 100);
};
setInterval(run, 1000);
複製代碼
泄漏了嗎?
var run = function () {
  var str = new Array(1000000).join('*');
  var doSomethingWithStr = function () {
    if (str === 'something')
      console.log("str was something");
  };
  doSomethingWithStr();
  var logIt = function () {
    console.log('interval');
  }
  setInterval(logIt, 100);
};
setInterval(run, 1000);
複製代碼

一旦一個變量被任一個閉包使用了,它會在全部的閉包詞法環境結束以後才被釋放,這會致使內存泄漏。

相關文章
相關標籤/搜索