Node——內(nèi)存控制

JS在瀏覽器中運(yùn)行的時(shí)候并不存在太大的內(nèi)存問題静稻,我們通常也不刻意的去優(yōu)化他們役拴,但是當(dāng)運(yùn)行在服務(wù)器端的時(shí)候辐赞,運(yùn)行時(shí)間長括享,這種問題就不得不考慮了搂根。

V8的垃圾回收機(jī)制與內(nèi)存限制

V8的內(nèi)存限制

在64位下只能使用1.4GB,在32位下0.7GB铃辖。即便你的物理內(nèi)存有32GB剩愧,單個(gè)Node進(jìn)程也只能使用這些內(nèi)存。如果你要將一個(gè)2G的文件讀到內(nèi)存里解析娇斩,good luck仁卷。
V8之所以要限制內(nèi)存的大小,是因?yàn)閂8垃圾回收的限制犬第。以1.5G的垃圾回收堆內(nèi)存為例锦积,V8做一次小的垃圾回收要50毫秒以上,做一次非增量式的垃圾回收要1秒以上歉嗓,這是在回收過程中JS線程被暫停的時(shí)間丰介,這是不可接受的。所以目前比較好的辦法就是限制住使用的內(nèi)存鉴分。
這個(gè)限制也不是不能打破哮幢,你可以選擇在啟動(dòng)時(shí)修改它:

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

這個(gè)只在初始化時(shí)生效,一旦生效就不能動(dòng)態(tài)改變了志珍。
新版本node的限制貌似取消了橙垢,至少我的機(jī)器到6個(gè)G時(shí)才報(bào)錯(cuò)的。

V8的垃圾回收機(jī)制

V8的垃圾回收機(jī)制主要基于分代式垃圾回收機(jī)制碴裙,將內(nèi)存分為新生代和老生代兩代钢悲,老生代是存活時(shí)間較長或常駐內(nèi)存的對(duì)象点额,新生代為存活時(shí)間較短的舔株。剛才那兩個(gè)命令就分別是對(duì)這兩個(gè)的設(shè)置。老生代的限制為1400/700MB还棱,新生代的是32/16MB载慈。
Scavenge算法
這個(gè)算法主要用在新生代內(nèi)存區(qū)域中,因?yàn)檫@個(gè)算法的主要思想是犧牲空間來換取時(shí)間的珍手。
算法將新生代內(nèi)存分為相等的兩份办铡,一個(gè)使用,一個(gè)閑置琳要。
處于使用狀態(tài)的空間成為From空間寡具,閑置的稱為To空間,當(dāng)我們分配對(duì)象時(shí)稚补,是在From空間中進(jìn)行分配的童叠。
當(dāng)垃圾回收開始時(shí),會(huì)檢查From空間中的存活對(duì)象课幕,將這些存活對(duì)象復(fù)制到To空間中厦坛,非存活的對(duì)象在這個(gè)過程中就被釋放掉了五垮。復(fù)制完成后,To和From空間互換杜秸。
可以看到放仗,它很快,但是費(fèi)空間撬碟,不過對(duì)于新生代這種少量的內(nèi)存來說是很劃算的诞挨。
在單純的Scavenge算法中,所有的存活對(duì)象都會(huì)被復(fù)制到To空間呢蛤,但是在分代垃圾回收的大背景下亭姥,有些存活對(duì)象會(huì)被復(fù)制到老生代內(nèi)存中。
當(dāng)這個(gè)對(duì)象已經(jīng)經(jīng)歷過一次Scavenge回收顾稀,它會(huì)被復(fù)制到老生代达罗;當(dāng)這個(gè)To空間已經(jīng)使用了超過25%時(shí),會(huì)被復(fù)制到老生代静秆。因?yàn)門o會(huì)在復(fù)制完成后變?yōu)镕rom粮揉,新的內(nèi)存分配在這里產(chǎn)生,它必須有足夠的空余空間抚笔。
**Mark-Sweep & Mark-Compact **
在老生代中使用上面的算法顯然是不可能的扶认。
這里首先使用Mark-Sweep。這是標(biāo)記清除法殊橙。它遍歷堆中的所有對(duì)象辐宾,并標(biāo)記活著的,在清除階段中清除所有未被標(biāo)記的對(duì)象膨蛮。
在新生代中叠纹,只復(fù)制活的,在老生代中敞葛,只清理死的誉察。這兩個(gè)都分別是兩部分中較少的那部分,所以這一整套垃圾回收比較高效惹谐。
在使用Mark-Sweep進(jìn)行清除后持偏,內(nèi)存變得不連續(xù)了,這對(duì)接下來的內(nèi)存分配會(huì)有影響氨肌,還會(huì)提前觸發(fā)下一次垃圾回收鸿秆。所以有了Mark-Compact,它將活著的對(duì)象往前移來填補(bǔ)空白怎囚。Mark-Compact過程是很慢的创南,V8只在空間不足分配新來的新生代時(shí)使用剃允。
**Incremental Marking **
因?yàn)槔厥丈婕皩?duì)程序?qū)ο蟮膭h除掐松,肯定需要將程序邏輯停下來,對(duì)于新生代來說不是什么問題埠戳,但是老生代就會(huì)很慢,于是有了增量標(biāo)記蕉扮,也就是垃圾回收與應(yīng)用邏輯交替進(jìn)行整胃。
同樣的還會(huì)有增量式整理和延遲清理。

高效使用內(nèi)存

作用域

在某個(gè)局部作用域中的對(duì)象會(huì)隨著局部作用域的銷毀而被釋放喳钟,在下次垃圾回收的時(shí)候就會(huì)清理掉這部分內(nèi)存屁使,如果全局作用域中的對(duì)象過多,那么這些對(duì)象存在的作用域直到繼承退出才會(huì)被釋放奔则,這些對(duì)象也會(huì)最終停留在老生代內(nèi)存區(qū)域中蛮寂。
如果你想手動(dòng)釋放一個(gè)變量,可以使用delete操作符易茬,但是并不推薦這樣做酬蹋,這樣做會(huì)干擾V8引擎的優(yōu)化,推薦使用將對(duì)象賦值為null或undefined來手動(dòng)釋放它抽莱。

閉包

閉包的使用使得JS有了許多優(yōu)秀的特性范抓,但是這樣也帶來了問題,一個(gè)閉包被賦值給一個(gè)變量以后食铐,這個(gè)閉包所在的作用域也就不會(huì)被銷毀匕垫,這個(gè)作用域中對(duì)象所使用的內(nèi)存也不會(huì)被釋放,這個(gè)要小心一下虐呻。

內(nèi)存指標(biāo)

進(jìn)程的內(nèi)存占用

使用process.memoryUsage()可以看到內(nèi)存的使用情況象泵。它返回的對(duì)象有3個(gè)屬性rss:進(jìn)程的常駐內(nèi)存部分;斟叼,heapTotal是堆中總共申請(qǐng)的內(nèi)存量偶惠;heapUsed表示目前堆中使用中的內(nèi)存量。
我們可以測(cè)試一下:

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

這個(gè)方法會(huì)不斷的分配內(nèi)存但不釋放犁柜,到最后:

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中,堆內(nèi)存占了大部分馋缅。

系統(tǒng)內(nèi)存的占用

使用os模塊中的函數(shù)來查看機(jī)器的物理內(nèi)存及其使用情況:

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

堆外內(nèi)存

從上面的結(jié)果中我們可以看到,堆內(nèi)存的總量總是小于rss绢淀。
我們將前面的useMem方法稍微改造一下萤悴,每一次構(gòu)造一個(gè)200M的對(duì)象:

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并未被分派到堆內(nèi)存中皆的,Buffer對(duì)象不同于其他對(duì)象覆履,它不經(jīng)過V8的內(nèi)存分配機(jī)制,所以也不會(huì)有堆內(nèi)存的大小限制。
這意味著利用堆外內(nèi)存可以突破內(nèi)存限制的問題硝全。

內(nèi)存泄露

內(nèi)存泄露在前端頁面上問題不太大栖雾,但是在服務(wù)器端就是個(gè)不得不考慮的問題。造成這個(gè)問題的原因有:

  • 緩存
  • 隊(duì)列消費(fèi)不及時(shí)
  • 作用域未釋放

慎將內(nèi)存用作緩存

緩存是很有效的節(jié)省IO的辦法伟众,但是在Node中析藕,一旦一個(gè)對(duì)象被當(dāng)做緩存來使用的時(shí)候就要格外的小心了,這意味著它將常駐在老生代內(nèi)存中凳厢,這樣的緩存越大意味著垃圾回收在做越多的無用功账胧。
所以創(chuàng)建一個(gè)有完善過期機(jī)制的緩存來控制緩存的增長是很有必要的。
可以通過限制鍵的數(shù)量等方法來控制緩存的增長先紫。
還有一個(gè)通常會(huì)被我們忽略的問題治泥,就是模塊的緩存由于模塊的緩存機(jī)制,它是常駐老生代的遮精。我們通過exports導(dǎo)出的函數(shù)是可以訪問文件模塊中的私有變量的居夹,這樣每個(gè)文件模塊在編譯執(zhí)行后形成的作用域由于模塊緩存的原因不會(huì)被釋放,所以設(shè)計(jì)模塊時(shí)要十分小心內(nèi)存泄露本冲。這里舉個(gè)例子:

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

這里每次調(diào)用leak方法吮播,都會(huì)導(dǎo)致局部變量leakArray不停的增加內(nèi)存的占用。
且進(jìn)程間無法共享內(nèi)存眼俊,在進(jìn)程內(nèi)使用緩存會(huì)造成進(jìn)程間緩存無法共享意狠,這對(duì)內(nèi)存是一種浪費(fèi)。如果需要大量緩存疮胖,最好使用進(jìn)程外緩存比如Redis和Memcached环戈。

關(guān)注隊(duì)列狀態(tài)

這也是一個(gè)不經(jīng)意產(chǎn)生的內(nèi)存泄露。隊(duì)列一般在消費(fèi)者-生產(chǎn)者模型中充當(dāng)中間人的角色澎灸,當(dāng)消費(fèi)大于生產(chǎn)時(shí)沒有問題院塞,但是當(dāng)生產(chǎn)大于消費(fèi)時(shí),會(huì)產(chǎn)生堆積性昭,就容易發(fā)生內(nèi)存泄露拦止。
比如收集日志,如果日志產(chǎn)生的速度大于文件寫入的速度糜颠,就容易產(chǎn)生內(nèi)存泄露汹族,表層的解決辦法是換用消費(fèi)速度更高的技術(shù),但是這不治本其兴。根本的解決方案應(yīng)該是監(jiān)控隊(duì)列的長度一旦堆積就報(bào)警或拒絕新的請(qǐng)求顶瞒,還有一種是所有的異步調(diào)用都有超時(shí)回調(diào),一旦達(dá)到時(shí)間調(diào)用未得到結(jié)果就報(bào)警元旬。

內(nèi)存泄露排查

node-heapdump
node-memwatch
這兩個(gè)模塊可以用來檢測(cè)內(nèi)存泄露榴徐,它們可以通過事件和抓取內(nèi)存快照的方式來為我們分析哪里有內(nèi)存泄露提供依據(jù)守问。

大內(nèi)存應(yīng)用

不可避免的我們會(huì)遇到大文件操作的問題。由于Node內(nèi)存的限制坑资,操作大內(nèi)存時(shí)要小心耗帕。stream模塊為我們提供了支持,這是一個(gè)原生模塊袱贮。

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

由于讀寫模式固定仿便,專門提供了一個(gè)pipe方法:

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末探越,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子窑业,更是在濱河造成了極大的恐慌钦幔,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件常柄,死亡現(xiàn)場離奇詭異鲤氢,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)西潘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門卷玉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人喷市,你說我怎么就攤上這事相种。” “怎么了品姓?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵寝并,是天一觀的道長。 經(jīng)常有香客問我腹备,道長衬潦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任植酥,我火速辦了婚禮镀岛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘友驮。我一直安慰自己漂羊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布喊儡。 她就那樣靜靜地躺著拨与,像睡著了一般。 火紅的嫁衣襯著肌膚如雪艾猜。 梳的紋絲不亂的頭發(fā)上买喧,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音匆赃,去河邊找鬼淤毛。 笑死,一個(gè)胖子當(dāng)著我的面吹牛算柳,可吹牛的內(nèi)容都是我干的低淡。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼瞬项,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼蔗蹋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起囱淋,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤猪杭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后妥衣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體皂吮,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年税手,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蜂筹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡芦倒,死狀恐怖艺挪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情兵扬,我是刑警寧澤麻裳,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站周霉,受9級(jí)特大地震影響掂器,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜俱箱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一国瓮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧狞谱,春花似錦乃摹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至伶跷,卻和暖如春掰读,著一層夾襖步出監(jiān)牢的瞬間秘狞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工蹈集, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留烁试,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓拢肆,卻偏偏與公主長得像减响,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子郭怪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 垃圾回收機(jī)制 nodejs在執(zhí)行JavaScript時(shí)支示,內(nèi)存受到v8限制,64位約為1.4g,32位0.7g 所有...
    wmtcore閱讀 7,838評(píng)論 0 7
  • 第一章 簡介 J2SE平臺(tái)的一大優(yōu)勢(shì)是它的自動(dòng)化內(nèi)存管理,避免了開發(fā)者去面對(duì)內(nèi)存管理的復(fù)雜性鄙才。 本文以Sun J2...
    tianyiliusha閱讀 945評(píng)論 0 1
  • 1 養(yǎng)狗前:下班后颖医,累得要死,直接癱軟裆蒸。 養(yǎng)狗后:下班后熔萧,累得要死,還得遛狗僚祷。 2 養(yǎng)狗前:獨(dú)自享受薯?xiàng)l炸雞佛致。 養(yǎng)...
    Chloe靜學(xué)姐閱讀 1,684評(píng)論 0 0
  • 以前,他最喜歡說辙谜,我最近對(duì)什么都沒耐心俺榆,我卻對(duì)他說,我對(duì)你一直有耐心装哆,其實(shí)罐脊,他不知道我有多喜歡他,多么的想和他一起...
    愛是繆斯閱讀 428評(píng)論 0 0
  • 人剛出生的時(shí)候,世界對(duì)他來說是陌生的凌简、未知的上炎,而又充滿了新鮮感。所以幼兒總是會(huì)用一雙清澈懵懂的眼睛來看周遭的一切雏搂。...
    竹韻悠然閱讀 525評(píng)論 0 2