1. 程式人生 > >Node.js記憶體管理基礎講解

Node.js記憶體管理基礎講解

Node與V8

  • 基本概念

    V8是Node的JavaScript執行引擎,V8引擎實際是一個高效能虛擬機器。Node在JavaScript的執行直接受益於V8,可以隨著V8的升級就能享受更好的效能或新的語言特性(如ES5和ES6)

  • 二者之間的關係

    1)大小限制說明

    對於一般的後端開發語言,基本記憶體使用是沒有限制的,但是在Node中通過javaScript使用記憶體時只能使用部分記憶體(64位系統下約為1.4G,32位系統下約為0.6G)

    2)限制的原因與特殊說明

    Node基於V8構建,所以在Node中使用javaScript基本都是通過V8自己的方式進行分配和管理的。但是Node的記憶體並不完全是通過V8進行分配管理的。檢視記憶體使用情況的時候,發現堆中的記憶體用量總是小於程序的常駐記憶體用量rss。Node中的記憶體使用並非都是通過V8進行分配的,還有一些不是通過V8進行分配的物件,我們稱之為堆外記憶體,堆外記憶體文章末尾會有一個說明(例如Buffer物件就不同於其他物件,他不經過V8的記憶體分配機制,不會有堆記憶體的限制)

    3)V8的物件分配

    V8中,所有的javaScript物件都是通過堆來進行分配的。V8的堆記憶體包括heapToal(已經申請到的堆記憶體),heapUsed(當前使用的堆記憶體);我們在程式碼中宣告變數並賦值的時候,所使用的物件的記憶體就分配在堆中。如果已申請的堆空閒記憶體不夠分配新的物件,將繼續申請堆記憶體,直到堆的大小超過V8的限制為止。

    說明:基於V8這種限制將會導致Node無法操作大記憶體物件,也因此後來出現了buffer這種不受V8丟記憶體控制的堆外記憶體管理。

開發過程中的那些不好回收的記憶體(高效使用記憶體)

由於V8已經對記憶體做了限制,我們應該做到高效的使用記憶體,讓垃圾回收機制更高效的工作,避免一些不容易回收記憶體的出現。

  • 作用域

    在JavaScript中,能形成作用域的有函式,with以及全域性作用域。

    1)作用域舉例最基本的記憶體回收過程

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

函式a在每次被呼叫的時候會建立對應的作用域,函式執行結束後,該作用域將會銷燬。同時因為該作用域中宣告的區域性變數分配在該作用域上,隨作用域的銷燬而銷燬。只被區域性變數引用的物件存活週期較短。程式碼中,由於物件較小,將會分配在新生代的Form空間中。作用域失效後,區域性變數local失效,其引用的物件將會在下次垃圾回收時被釋放。

2)作用域中的變數查詢

JavaScript在執行時會查詢變數定義在哪,最先查詢的當前作用域,當前作用域沒有,會向上級的作用域查詢,直到最頂層全域性作用域查到,如果沒有最後返回undefine。
3)變數的主動釋放

如果變數是全域性變數(通過var宣告或定義在global變數上),全域性作用域直到程序退出才能釋放,這種情況將導致引用的物件常駐記憶體(常駐在老生代中)。這種需要釋放常駐記憶體中的物件,可以使用delete操作來刪除引用關係,或者將變數重新賦值,讓舊物件脫離引用關係(也就是物件的引用即所佔的記憶體空間原本指向某個變數現在指向空獲未定義),這樣在接下來的老生代記憶體 清 除和整理的過程中會被釋放。

global.foo="i am gang";
console.log(global.foo);// i am gang

delete global.foo;

//或者重新賦值
global.foo=undefined;// or null
console.log(global.foo);//undefined

說明:雖然兩種方式都可以主動釋放變數引用的物件(也就是那一小塊記憶體),但是推薦大家使用重新賦值的方法,因為在V8中通過delete刪除物件的屬性有可能干擾V8的優化。

  • 閉包

    在javaScript中,實現外部作用域訪問內部作用域中變數的方法叫做閉包(closure)。這得益於高階函式的特性:函式可以作為引數或者返回值。
    閉包它實現了外部作用域訪問內部作用域中變數的方法。這句話需要好好理解。

    簡單例子說明閉包
    兩段程式碼對比:

var A=function(){
    (function(){
        var local="區域性變數";
    }());
    console.log(local); //local未定義異常
}

var B=function(){
    var C=function(){
        var local="區域性變數";
        return function(){
            return local;
        };
    };
    var c=C();
    console.log(c()); //區域性變數
};

分析第二段程式碼,函式C執行完成後,區域性變數local會隨著作用域的銷燬而被回收。但是注意這裡的特點是返回值是一個匿名函式,而且這個函式中具備了訪問local的條件,後面的程式碼執行,外部作用域是無法直接訪問local的,但是若要訪問它,只要通過這個中間函式稍作週轉即可。以上就是閉包的基本分析,現在能夠更好的理解我畫重點的那句話了吧。

記憶體相關基本命令使用

  • V8中記憶體使用情況檢視:
$ node
> process.memoryUsage();
{
    rss:14958592,
    heapTotal:7195904,
    heapUsed:2821496
}

heapTotal:V8中已申請的堆記憶體

heapUsed:V8中當前使用的堆記憶體

rss:程序的常駐記憶體部分

  • 檢視系統的記憶體佔用
$ node
> os.totalmem()
82132131
> os.freemem()
 31273127

os.totalmem 作業系統的總記憶體

os.freemem 作業系統的閒置記憶體

  • 堆外記憶體

    檢視v8記憶體使用情況,process.memoryUsage()的結果可以看到,V8堆中的記憶體用量總是小於程序的常駐記憶體用量rss,也就是說Node中的記憶體使用並非都是V8控制,還有一部分不是通過V8分配的(rss-heaptotal這部分),不通過V8分配的記憶體稱之為堆外記憶體。

    使用buffer每次構造200MB的記憶體,程式碼如下:

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

程式碼執行過程中,檢視記憶體使用情況會發現到最後,V8的使用記憶體heapUsed和申請的記憶體heaptotal基本不變,而常駐記憶體rss在不斷增加,可以看出buffer物件不同於其它物件,不經過V8記憶體分配機制,不會有堆記憶體的限制。後面的文章會對buffer進行詳細的講解。

記憶體洩露

Node對記憶體洩漏十分敏感,一旦線上應用有成千上萬的流量,哪怕一個位元組的記憶體洩漏也會造成堆積,垃圾回收過程中將會耗費更多時間進行物件掃描,應用響應緩慢,直到程序記憶體溢位,應用奔潰。

  • 記憶體洩漏的本質

    應當回收的物件出現意外而沒有被回收,變成常駐在老生代中的物件。

  • 造成記憶體洩漏的原因

    1)作用域未釋放
    2)佇列消費不及時
    3) 作用域未釋放