Node——內存控制

JS在瀏覽器中運行的時候並不存在太大的內存問題,我們通常也不刻意的去優化他們,但是當運行在服務器端的時候,運行時間長,這種問題就不得不考慮了。

V8的垃圾回收機制與內存限制

V8的內存限制

在64位下只能使用1.4GB,在32位下0.7GB。即便你的物理內存有32GB,單個Node進程也只能使用這些內存。如果你要將一個2G的文件讀到內存裏解析,good luck。
V8之所以要限制內存的大小,是因爲V8垃圾回收的限制。以1.5G的垃圾回收堆內存爲例,V8做一次小的垃圾回收要50毫秒以上,做一次非增量式的垃圾回收要1秒以上,這是在回收過程中JS線程被暫停的時間,這是不可接受的。所以目前比較好的辦法就是限制住使用的內存。
這個限制也不是不能打破,你可以選擇在啓動時修改它:

node --max-old-space-size=1700 test.js // 單位爲MB 
node --max-new-space-size=1024 test.js // 單位爲KB 

這個只在初始化時生效,一旦生效就不能動態改變了。
新版本node的限制貌似取消了,至少我的機器到6個G時才報錯的。

V8的垃圾回收機制

V8的垃圾回收機制主要基於分代式垃圾回收機制,將內存分爲新生代和老生代兩代,老生代是存活時間較長或常駐內存的對象,新生代爲存活時間較短的。剛纔那兩個命令就分別是對這兩個的設置。老生代的限制爲1400/700MB,新生代的是32/16MB。
Scavenge算法
這個算法主要用在新生代內存區域中,因爲這個算法的主要思想是犧牲空間來換取時間的。
算法將新生代內存分爲相等的兩份,一個使用,一個閒置。
處於使用狀態的空間成爲From空間,閒置的稱爲To空間,當我們分配對象時,是在From空間中進行分配的。
當垃圾回收開始時,會檢查From空間中的存活對象,將這些存活對象複製到To空間中,非存活的對象在這個過程中就被釋放掉了。複製完成後,To和From空間互換。
可以看到,它很快,但是費空間,不過對於新生代這種少量的內存來說是很划算的。
在單純的Scavenge算法中,所有的存活對象都會被複制到To空間,但是在分代垃圾回收的大背景下,有些存活對象會被複制到老生代內存中。
當這個對象已經經歷過一次Scavenge回收,它會被複制到老生代;當這個To空間已經使用了超過25%時,會被複制到老生代。因爲To會在複製完成後變爲From,新的內存分配在這裏產生,它必須有足夠的空餘空間。
Mark-Sweep & Mark-Compact
在老生代中使用上面的算法顯然是不可能的。
這裏首先使用Mark-Sweep。這是標記清除法。它遍歷堆中的所有對象,並標記活着的,在清除階段中清除所有未被標記的對象。
在新生代中,只複製活的,在老生代中,只清理死的。這兩個都分別是兩部分中較少的那部分,所以這一整套垃圾回收比較高效。
在使用Mark-Sweep進行清除後,內存變得不連續了,這對接下來的內存分配會有影響,還會提前觸發下一次垃圾回收。所以有了Mark-Compact,它將活着的對象往前移來填補空白。Mark-Compact過程是很慢的,V8只在空間不足分配新來的新生代時使用。
Incremental Marking
因爲垃圾回收涉及對程序對象的刪除,肯定需要將程序邏輯停下來,對於新生代來說不是什麼問題,但是老生代就會很慢,於是有了增量標記,也就是垃圾回收與應用邏輯交替進行。
同樣的還會有增量式整理和延遲清理。

高效使用內存

作用域

在某個局部作用域中的對象會隨着局部作用域的銷燬而被釋放,在下次垃圾回收的時候就會清理掉這部分內存,如果全局作用域中的對象過多,那麼這些對象存在的作用域直到繼承退出纔會被釋放,這些對象也會最終停留在老生代內存區域中。
如果你想手動釋放一個變量,可以使用delete操作符,但是並不推薦這樣做,這樣做會干擾V8引擎的優化,推薦使用將對象賦值爲null或undefined來手動釋放它。

閉包

閉包的使用使得JS有了許多優秀的特性,但是這樣也帶來了問題,一個閉包被賦值給一個變量以後,這個閉包所在的作用域也就不會被銷燬,這個作用域中對象所使用的內存也不會被釋放,這個要小心一下。

內存指標

進程的內存佔用

使用process.memoryUsage()可以看到內存的使用情況。它返回的對象有3個屬性rss:進程的常駐內存部分;,heapTotal是堆中總共申請的內存量;heapUsed表示目前堆中使用中的內存量。
我們可以測試一下:

 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('-----------------------------------------------------------'); 
 };
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 < 150; j++) {   
    showMem();   
    total.push(useMem()); 
} 
showMem();

這個方法會不斷的分配內存但不釋放,到最後:

Process: heapTotal 6086.95 MB heapUsed 6083.24 MB rss 6099.39 MB ---------------------------------------------------------------- FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory 

這裏可以看到,在所有的rss中,堆內存佔了大部分。

系統內存的佔用

使用os模塊中的函數來查看機器的物理內存及其使用情況:

var os = require("os");
console.log(os.totalmem());
console.log(os.freemem());

堆外內存

從上面的結果中我們可以看到,堆內存的總量總是小於rss。
我們將前面的useMem方法稍微改造一下,每一次構造一個200M的對象:

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; 
};
Process: heapTotal 5.85 MB heapUsed 1.85 MB rss 3012.91 MB 

可以看到,這裏buffer並未被分派到堆內存中,Buffer對象不同於其他對象,它不經過V8的內存分配機制,所以也不會有堆內存的大小限制。
這意味着利用堆外內存可以突破內存限制的問題。

內存泄露

內存泄露在前端頁面上問題不太大,但是在服務器端就是個不得不考慮的問題。造成這個問題的原因有:

  • 緩存
  • 隊列消費不及時
  • 作用域未釋放

慎將內存用作緩存

緩存是很有效的節省IO的辦法,但是在Node中,一旦一個對象被當做緩存來使用的時候就要格外的小心了,這意味着它將常駐在老生代內存中,這樣的緩存越大意味着垃圾回收在做越多的無用功。
所以創建一個有完善過期機制的緩存來控制緩存的增長是很有必要的。
可以通過限制鍵的數量等方法來控制緩存的增長。
還有一個通常會被我們忽略的問題,就是模塊的緩存由於模塊的緩存機制,它是常駐老生代的。我們通過exports導出的函數是可以訪問文件模塊中的私有變量的,這樣每個文件模塊在編譯執行後形成的作用域由於模塊緩存的原因不會被釋放,所以設計模塊時要十分小心內存泄露。這裏舉個例子:

var leakArray = []; 
exports.leak = function () {   
    leakArray.push("leak" + Math.random()); 
};

這裏每次調用leak方法,都會導致局部變量leakArray不停的增加內存的佔用。
且進程間無法共享內存,在進程內使用緩存會造成進程間緩存無法共享,這對內存是一種浪費。如果需要大量緩存,最好使用進程外緩存比如Redis和Memcached。

關注隊列狀態

這也是一個不經意產生的內存泄露。隊列一般在消費者-生產者模型中充當中間人的角色,當消費大於生產時沒有問題,但是當生產大於消費時,會產生堆積,就容易發生內存泄露。
比如收集日誌,如果日誌產生的速度大於文件寫入的速度,就容易產生內存泄露,表層的解決辦法是換用消費速度更高的技術,但是這不治本。根本的解決方案應該是監控隊列的長度一旦堆積就報警或拒絕新的請求,還有一種是所有的異步調用都有超時回調,一旦達到時間調用未得到結果就報警。

內存泄露排查

node-heapdump
node-memwatch
這兩個模塊可以用來檢測內存泄露,它們可以通過事件和抓取內存快照的方式來爲我們分析哪裏有內存泄露提供依據。

大內存應用

不可避免的我們會遇到大文件操作的問題。由於Node內存的限制,操作大內存時要小心。stream模塊爲我們提供了支持,這是一個原生模塊。

var fs = require("fs");
var reader = fs.createReadStream('in.txt'); 
var writer = fs.createWriteStream('out.txt'); 
reader.on('data', function (chunk) {   
    writer.write(chunk); 
    console.log(chunk);
}); 
reader.on('end', function () {  
    writer.end(); 
}); 

由於讀寫模式固定,專門提供了一個pipe方法:

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

如果並不是字符串層面的操作,則可以使用純粹的Buffer來操作。

發佈了128 篇原創文章 · 獲贊 6 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章