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來操作字柠。