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

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

V8的內(nèi)存限制

在一般的后端開發(fā)語言中,在基本的內(nèi)存使用上沒有什么限制,然而在Node中通過JavaScript使用內(nèi)存時(shí)就會(huì)發(fā)現(xiàn)只能使用部分內(nèi)存(64位系統(tǒng)下約為1.4GB,32位系統(tǒng)下約為0.7GB),在這樣的限制下,將會(huì)導(dǎo)致Node無法直接操作大內(nèi)存對象棒拂,比如無法將一個(gè)2GB的文件讀入內(nèi)存中進(jìn)行字符串分析處理。

造成這個(gè)問題的主要原因在于Node基于V8構(gòu)建玫氢,所以在Node中使用的JavaScript對象基本都是通過V8自己的方式來進(jìn)行分配和管理的帚屉。V8的這套內(nèi)存管理機(jī)制在瀏覽器的應(yīng)用場景下使用起來綽綽有余,但在Node中漾峡,卻限制了開發(fā)者隨心使用大內(nèi)存的想法攻旦。

V8對象分配

在V8中所有的JavaScript對象都是通過堆來進(jìn)行分配的,在Node中使用process.memoryUsage()能夠得到內(nèi)存的使用信息對象生逸,其中heapTotal 和 heapUsed 代表 V8 的內(nèi)存使用情況牢屋,已申請的和已使用的。 external 代表 V8 管理的槽袄,綁定到 Javascript 的 C++ 對象的內(nèi)存使用情況烙无。 rss 是駐留集大小, 是給這個(gè)進(jìn)程分配了多少物理內(nèi)存(占總分配內(nèi)存的一部分),這些物理內(nèi)存中包含堆遍尺、代碼段截酷、以及棧。

在當(dāng)我們在代碼中聲明變量并賦值時(shí)狮鸭,所使用對象的內(nèi)存就分配在堆中合搅,如果已申請的堆空閑內(nèi)存不夠分配新的對象多搀,將繼續(xù)申請堆內(nèi)存歧蕉,直到堆的大小超過V8的限制為止。

V8為什么要限制堆的大小

表層原因是V8最初為瀏覽器而設(shè)計(jì)康铭,不太可能遇到大量內(nèi)存的使用場景惯退。深層原因是V8的垃圾回收機(jī)制的限制,按官方的說法从藤,以1.5GB的垃圾回收堆為例催跪,V8做一次小的垃圾回收需要50毫秒以上锁蠕,做一次非增量式的垃圾回收甚至要1秒以上。這里的時(shí)間指的是在垃圾回收中引起JavaScript線程暫停執(zhí)行的時(shí)間懊蒸。很明顯荣倾,這是無法接受的,因此直接限制了堆內(nèi)存的大小骑丸。

當(dāng)然舌仍,這個(gè)限制也可以打開,Node在啟動(dòng)時(shí)可以傳遞--max-old-space-size或 --max-new-space-size來調(diào)整內(nèi)存限制的大小

node --max-old-space-size=1700 test.js // 單位為MB 設(shè)置老生代內(nèi)存空間的最大值

node --max-new-space-size=1024 test.js // 單位為KB 設(shè)置新生代內(nèi)存空間的最大值

只能在初始化的時(shí)候設(shè)置通危,一旦生效就不能再動(dòng)態(tài)改變铸豁。

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

在V8中,主要將內(nèi)存分為新生代和老生代兩代菊碟,新生代中的對象為存活時(shí)間較短的對象节芥,老生代中的對象為存活時(shí)間較長或常駐內(nèi)存的對象。

V8的整體大小就是新生代所用內(nèi)存空間加上老生代的內(nèi)存空間逆害,在64位系統(tǒng)中头镊,老生代內(nèi)存為1400MB,新生代內(nèi)存為32MB忍燥,在32位系統(tǒng)中拧晕,則分別為700MB、16MB梅垄。又因?yàn)樾律鷥?nèi)存有2個(gè)厂捞,所以在64位系統(tǒng)中,V8堆內(nèi)存的最大值為1464MB队丝,而在32位系統(tǒng)中則為732MB靡馁。

新生代:Scavenge算法

新生代中的對象主要通過Scavenge算法進(jìn)行垃圾回收,它將新生代內(nèi)存一分為二机久,每一部分的空間成為semispace(半空間)臭墨,在這兩個(gè)semispace中,只有一個(gè)處于使用中膘盖,成為From空間胧弛;另一個(gè)處于閑置狀態(tài),稱為To空間侠畔。當(dāng)我們分配對象時(shí)结缚,先是在From空間中進(jìn)行分配,當(dāng)開始進(jìn)行垃圾回收時(shí)软棺,會(huì)檢查From空間中的存活對象红竭,這些存活對象將被復(fù)制到To空間中,而非存活對象占用的空間將被釋放,完成賦值后茵宪,F(xiàn)rom空間和To空間的角色發(fā)生對換最冰。

Scavenge是典型的犧牲空間換取時(shí)間的算法,但Scavenge由于只復(fù)制存活的對象稀火,并且對于生命周期短的場景存活對象只占少部分暖哨,所以它在事件效率上有優(yōu)異的表現(xiàn),而新生代中對象的生命周期較短凰狞,恰恰適合這個(gè)算法阶捆。

當(dāng)一個(gè)對象經(jīng)過多次復(fù)制依然存活時(shí)贞言,它將會(huì)被認(rèn)為是生命周期較長的對象,這種對象隨后會(huì)被移動(dòng)到老生代中,這個(gè)過程被稱為晉升历谍。

對象晉升的條件主要有兩個(gè)法精,一個(gè)是是否經(jīng)歷過Scavenge回收粥谬,一個(gè)是To空間的內(nèi)存占用比超過限制进胯。

對象從From空間中復(fù)制到To空間時(shí):

第一種情況:檢查它的內(nèi)存地址來判斷這個(gè)對象是否已經(jīng)經(jīng)歷過一次Scavenge回收,是就直接復(fù)制到老生代空間中粉渠,沒有就復(fù)制到To空間分冈。

第二種情況:判斷To空間中是否已經(jīng)使用超過了25%,如果超過了霸株,則直接晉升到老生代空間中雕沉,否則就復(fù)制To空間。設(shè)置25%這個(gè)限制值的原因是當(dāng)這次Scavenge回收完成后去件,這個(gè)To空間就將變成From空間坡椒,接下來的內(nèi)存分配會(huì)在這個(gè)空間中進(jìn)行,如果占比較高尤溜,會(huì)影響后續(xù)的內(nèi)存分配倔叼。

老生代:Mark-Sweep & Mark-Compact

對于老生代存活對象占較大比重,那么繼續(xù)采用Scavenge算法復(fù)制存活對象將效率很低宫莱,而且Scavenge算法會(huì)浪費(fèi)一半的空間丈攒。為此V8在老生代主要采用了Mark-Sweep & Mark-Compact相結(jié)合的方式進(jìn)行垃圾回收。

Mark-Sweep是標(biāo)記清除的意思授霸,它分為標(biāo)記和清除兩個(gè)階段巡验,它會(huì)在標(biāo)記階段遍歷堆中的所有對象,并標(biāo)記活著的對象碘耳,在隨后的清除階段階段只清除沒有被標(biāo)記的對象显设,可以看出,Scavenge中只復(fù)制活著的對象藏畅,而Mark-Sweep只清理死亡對象敷硅,活對象在新生代中只占較小部分,死對象在老生代中只占較小部分愉阎,這是在新老生代使用這兩種不同回收方式的原因绞蹦。

Mark-Sweep最大的問題在于進(jìn)行一次標(biāo)記清楚回收后,內(nèi)存空間會(huì)出現(xiàn)不連續(xù)的狀態(tài)榜旦,下圖中黑色的部分就會(huì)死亡對象幽七,清除之后就出現(xiàn)這種個(gè)問題,這種內(nèi)存碎片會(huì)對后續(xù)的內(nèi)存分配造成問題溅呢,因?yàn)楹芸赡艹霈F(xiàn)需要分配一個(gè)大對象的情況澡屡,這時(shí)所有的碎片空間都無法完成這次分配,就會(huì)提前觸發(fā)垃圾回收咐旧,而這次回收是不必要的驶鹉。

mark-sweep.png

而Mark-Compact就是為了解決Mark-Sweep的內(nèi)存碎片問題,是指標(biāo)記整理的意思铣墨,是在Mark-Sweep的基礎(chǔ)上演變而來的室埋,他們的差別在于對象在標(biāo)記死亡后,在整理的過程中伊约,將活著的對象往一端移動(dòng)姚淆,移動(dòng)完成后,直接清理調(diào)邊界外的內(nèi)存屡律。

mark-compact.png

在V8中這兩種回收策略是結(jié)合使用的腌逢,但是由于Mark-Compact需要移動(dòng)對象,所以它的執(zhí)行速度不可能很快超埋,所以在取舍上搏讶,V8主要使用Mark-Sweep,在空間不足以對新生代中晉升過來的對象進(jìn)行分配時(shí)才使用Mark-Compact霍殴。

{{% notice info %}}
為了避免出現(xiàn)JavaScript應(yīng)用邏輯與垃圾回收器看到的不一致的情況窍蓝,垃圾回收的3種基本算法都需要將應(yīng)用邏輯暫停下來,待執(zhí)行完垃圾回收后再恢復(fù)執(zhí)行應(yīng)用邏輯繁成,這種行為被成為“全停頓”(stop-the-world)
{{% /notice %}}

在V8的分代式垃圾回收中吓笙,一次小垃圾回收只收集新生代,由于新生代默認(rèn)配置得較小巾腕,且其中存活對象通常較少面睛,所以即便它是全停頓的影響也不大, 但老生代通常配置得較大尊搬,且存活對象較多叁鉴,全堆垃圾回收的標(biāo)記、清理佛寿、整理等動(dòng)作造成的停頓就比較可怕幌墓,需要設(shè)法改善但壮。

為了降低這種停頓時(shí)間,V8先從標(biāo)記階段入手常侣,將原本要一口氣停頓完成的動(dòng)作改為了增量標(biāo)記(incremental marking)蜡饵,也就是拆分為許多小“步進(jìn)”,每做完一次“步進(jìn)”胳施,就讓JavaScript應(yīng)用邏輯執(zhí)行一小會(huì)兒溯祸,垃圾回收與應(yīng)用邏輯交替執(zhí)行直到標(biāo)記階段完成。

V8在經(jīng)過增量標(biāo)記的改進(jìn)后舞肆,垃圾回收的最大停頓時(shí)間可以減少到原本的1/6左右焦辅,V8后續(xù)還移入了延遲清理(lazy sweeping)和增量式整理(incremental compaction),讓清理和整理動(dòng)作也變成增量式的椿胯,同時(shí)還計(jì)劃引入并行標(biāo)記和并行清理筷登,進(jìn)一步利用多核性能降低每次停頓的時(shí)間。

{{% notice tip %}}
對于V8的垃圾回收特點(diǎn)和JavaScript在單線程上的執(zhí)行情況哩盲,垃圾回收是影響性能的因素之一仆抵,想要高性能的執(zhí)行效率,需要注意讓垃圾回收盡量收的進(jìn)行种冬,尤其是全堆垃圾回收镣丑。
{{% /notice %}}

高效使用內(nèi)存

作用域

函數(shù)在每次調(diào)用時(shí)會(huì)創(chuàng)建對應(yīng)的作用域,函數(shù)執(zhí)行結(jié)束后娱两,該作用域?qū)?huì)銷毀莺匠,同時(shí)作用域中聲明的局部變量分配在該作用域上,隨著作用域的銷毀而銷毀十兢,只被局部變量引用的對象存活周期較短趣竣,在作用域釋放之后,局部變量就會(huì)失效旱物,其引用的對象將會(huì)在下次垃圾回收時(shí)內(nèi)被釋放遥缕。

JavaScript在執(zhí)行時(shí)回去查找該變量定義在哪里,它最先查找的是當(dāng)前作用域宵呛,如果在當(dāng)前作用域中無法找到該變量的聲明单匣,將會(huì)向上級(jí)的作用域里查找,直到查到為止宝穗。

變量的主動(dòng)釋放

如果變量是全局變量户秤,由于全局作用域需要直到進(jìn)程退出才能釋放,此時(shí)將導(dǎo)致引用的對象常駐在老生代中逮矛,如果需要釋放常駐內(nèi)存的對象鸡号,可以使用delete操作來刪除引用關(guān)系,或者將變量重新賦值须鼎,讓舊的對象脫離引用關(guān)系鲸伴,在接下來的老生代內(nèi)存清除和整理的過程中府蔗,會(huì)被回收釋放。

global.foo = 'abc';
delete global.foo;

// 或者重新賦值
global.foo = undefined // or null;

{{% notice tip %}}
同樣汞窗,如果在非全局作用域中姓赤,想要主動(dòng)釋放變量引用的對象,也可以通過這樣的方式杉辙,雖然delete操作和重新賦值具有相同的效果,但是在V8中通過delete刪除對象的屬性有可能干擾V8的優(yōu)化捶朵,所以通過賦值方式解除引用更好蜘矢。
{{% /notice %}}

閉包

在JavaScript中,實(shí)現(xiàn)外部作用域訪問內(nèi)部作用域中的變量的方法叫做閉包(closure)综看,這得益于高階函數(shù)的特性品腹,函數(shù)可以作為參數(shù)或者返回值。

雖說局部變量將會(huì)隨著作用域的銷毀而被回收红碑,但是閉包返回的是一個(gè)匿名函數(shù)舞吭,這個(gè)函數(shù)中具備了訪問局部變量的條件,雖然在后續(xù)的執(zhí)行中析珊,外部作用域還是無法直接返回局部變量羡鸥,但是若要訪問它,只要通過這個(gè)中間函數(shù)稍作周轉(zhuǎn)即可忠寻。

閉包是JavaScript的高級(jí)特性惧浴,利用它可以產(chǎn)生很多巧妙的效果,它的問題在于奕剃,一旦有變量引用這個(gè)中間函數(shù)衷旅,這個(gè)中間函數(shù)就不會(huì)被釋放,同時(shí)也會(huì)使原始的作用域不會(huì)得到釋放纵朋,作用域中產(chǎn)生的內(nèi)存占用也不會(huì)得到釋放柿顶,除非不再引用,才會(huì)逐步釋放操软。在正常的JavaScript執(zhí)行中嘁锯,無法立即回收的內(nèi)存有閉包和全局變量引用這兩種情況,由于V8的內(nèi)存限制聂薪,要十分小心此類變量是否無限制地增加猪钮,因?yàn)樗鼤?huì)導(dǎo)致老生代中的對象增多。

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

前面我們提到了process.memoryUsage()可以查看內(nèi)存使用的情況胆建,除此之外烤低,os模塊中的totalmem()和freemem()方法也可以查看內(nèi)存使用情況,這兩個(gè)方法用于查看操作系統(tǒng)的內(nèi)存使用情況笆载,它們分別返回系統(tǒng)的總內(nèi)存和閑置內(nèi)存扑馁。

$ node
> process.memoryUsage()
{ rss: 13852672,
heapTotal: 6131200,
heapUsed: 2757120 }

其中rss是resident set size的簡寫涯呻,即進(jìn)程的常駐內(nèi)存部分,進(jìn)程的內(nèi)存總共有幾部分腻要,一部分是rss复罐,其余部分在交換區(qū)(swap)或者文件系統(tǒng)(filesystem)中,這3個(gè)值的單位都是字節(jié)雄家。


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 < 15; j++) {
  showMem();
  total.push(useMem());
}
showMem();
$ node outofmemory.js
Process: heapTotal 3.86 MB heapUsed 2.10 MB rss 11.16 MB
----------------------------------------------------------------
Process: heapTotal 357.88 MB heapUsed 353.95 MB rss 365.44 MB
----------------------------------------------------------------
Process: heapTotal 520.88 MB heapUsed 513.94 MB rss 526.30 MB
----------------------------------------------------------------
Process: heapTotal 679.91 MB heapUsed 673.86 MB rss 686.14 MB
----------------------------------------------------------------
Process: heapTotal 839.93 MB heapUsed 833.86 MB rss 846.16 MB
----------------------------------------------------------------
Process: heapTotal 999.94 MB heapUsed 993.86 MB rss 1006.93 MB
----------------------------------------------------------------
Process: heapTotal 1159.96 MB heapUsed 1153.86 MB rss 1166.95 MB
----------------------------------------------------------------
Process: heapTotal 1367.99 MB heapUsed 1361.86 MB rss 1375.00 MB
----------------------------------------------------------------
FATAL ERROR: CALL_AND_RETRY_2 Allocation failed - process out of memory

可以看到效诅,每次調(diào)用useMem到導(dǎo)致了3個(gè)值的增長,在接近1500MB的時(shí)候趟济,無法繼續(xù)分配內(nèi)存乱投,然后進(jìn)程內(nèi)存溢出了,連循環(huán)體都無法執(zhí)行完成顷编,僅執(zhí)行了7次戚炫。

通過process.memoryUsage()的結(jié)果可以看到,堆中的內(nèi)存用量總是小于進(jìn)程的常駐內(nèi)存用量媳纬,這意味著Node中的內(nèi)存使用并非都是通過V8進(jìn)行分配的双肤,我們將那些不是通過V8分配的內(nèi)存稱為堆外內(nèi)存。

在這里我們修改useMem方法钮惠,將Array改為Buffer茅糜,將size變大,每一次構(gòu)造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;
};

重新執(zhí)行該代碼:

$ node out_of_heap.js
Process: heapTotal 3.86 MB heapUsed 2.07 MB rss 11.12 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.94 MB rss 212.88 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.95 MB rss 412.89 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.95 MB rss 612.89 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.92 MB rss 812.89 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.92 MB rss 1012.89 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.84 MB rss 1212.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.84 MB rss 1412.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.84 MB rss 1612.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.84 MB rss 1812.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.84 MB rss 2012.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.84 MB rss 2212.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.84 MB rss 2412.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.85 MB rss 2612.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.85 MB rss 2812.91 MB
----------------------------------------------------------------
Process: heapTotal 5.85 MB heapUsed 1.85 MB rss 3012.91 MB
----------------------------------------------------------------

我們看到15次循環(huán)都完整執(zhí)行限匣,并且三個(gè)內(nèi)存占用值與前一個(gè)示例完全不同,heapTotal和heapUsed變化極小毁菱,唯一變化的是rss的值米死,并且該值以及遠(yuǎn)遠(yuǎn)超過V8的限制值,這其中的原因是Buffer對象不同于其他對象贮庞,它不經(jīng)過V8的內(nèi)存分配機(jī)制峦筒,所以也不會(huì)有堆內(nèi)存的大小限制,這意味著使用堆外內(nèi)存可以突破內(nèi)存限制的問題窗慎。

為什么Bufer對象并非通過V8分配物喷,這在于Node并不同于瀏覽器的應(yīng)用場景,在瀏覽器中遮斥,JavaScript直接處理字符串即可滿足絕大多數(shù)的業(yè)務(wù)需求峦失,而Node則需要處理網(wǎng)絡(luò)流和文件I/O流,操作字符串遠(yuǎn)遠(yuǎn)不能滿足傳輸?shù)男阅苄枨笫趼穑虼薔ode的內(nèi)存構(gòu)成主要由通過V8進(jìn)行分配的部分和Node自行分配的部分尉辑,受V8的垃圾回收限制的主要是V8的堆內(nèi)存。

內(nèi)存泄漏

內(nèi)存泄漏會(huì)造成堆積较屿,垃圾回收過程中將會(huì)消耗更多時(shí)間進(jìn)行對象掃描隧魄,應(yīng)用響應(yīng)緩慢卓练,直到進(jìn)程內(nèi)存溢出,應(yīng)用崩潰购啄。盡管內(nèi)存泄漏的情況不盡相同襟企,但其實(shí)質(zhì)只有一個(gè),那就是應(yīng)當(dāng)回收的對象出現(xiàn)意外而沒有被回收狮含,變成了常駐的老生代中的對象顽悼。通常造成內(nèi)存泄漏的原因有如下幾個(gè):緩存、隊(duì)列消費(fèi)不及時(shí)几迄、作用域未釋放蔚龙。

慎將內(nèi)存當(dāng)做緩存

JavaScript開發(fā)者通常喜歡用對象的鍵值對來緩存東西,但這與嚴(yán)格意義上的緩存又有著區(qū)別乓旗,嚴(yán)格意義的緩存有著完善的過期策略府蛇,而普通的鍵值對并沒有集索。

_.memoize = function(func, hasher) {
  var memo = {};
  hasher || (hasher = _.identity);
  return function() {
    // 根據(jù)函數(shù)的參數(shù)形成不同的hash值屿愚,以此為鍵將結(jié)果緩存在memo上。
    var key = hasher.apply(this, arguments);
    return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
  };
};

它的原理是以參數(shù)作為鍵進(jìn)行緩存务荆,以內(nèi)存空間換CPU執(zhí)行時(shí)間妆距,這里潛藏的陷阱即是每個(gè)被執(zhí)行的結(jié)果都會(huì)按參數(shù)緩存在memo對象上,不會(huì)被清楚函匕。這在前端網(wǎng)頁這種短時(shí)應(yīng)用場景中不存在大問題娱据,但是執(zhí)行量大和參數(shù)多樣性的情況,會(huì)造成內(nèi)存占用不釋放盅惜,如果需要中剩,可以限制緩存對象的大小,加上過期策略以防止內(nèi)存無限制增長抒寂。

var LimitableMap = function (limit) {
  this.limit = limit || 10;
  this.map = {};
  this.keys = [];
};
var hasOwnProperty = Object.prototype.hasOwnProperty;

LimitableMap.prototype.set = function (key, value) {
  var map = this.map;
  var keys = this.keys;
  if (!hasOwnProperty.call(map, key)) {
    // 如果沒有緩存结啼,判斷對象擁有的屬性數(shù)量是否超過限制
    if (keys.length === this.limit) {
      // 超過限制,淘汰第一次緩存的數(shù)據(jù)屈芜,再緩存
      var firstKey = keys.shift();
      delete map[firstKey];
    }
    // 未超過限制郊愧,緩存起來
    keys.push(key);
  }
  // 如果有緩存,直接替換
  map[key] = value;
};

LimitableMap.prototype.get = function (key) {
  return this.map[key];
};
module.exports = LimitableMap;

另一個(gè)案例在于模塊機(jī)制井佑,為了加速模塊的引入属铁,所有的模塊都會(huì)通過編譯執(zhí)行,然后被緩存起來躬翁,由于通過exports導(dǎo)出的函數(shù)可以訪問文件模塊中的私有變量焦蘑,這樣每個(gè)文件模塊在編譯執(zhí)行后形成的作用域因?yàn)槟K緩存的原因不會(huì)被釋放。

由于模塊的緩存機(jī)制盒发,模塊是常駐老生代的喇肋,所有在設(shè)計(jì)模塊時(shí)坟乾,要十分小心內(nèi)存泄漏的情況。也可以添加清空隊(duì)列的相應(yīng)接口蝶防,以供調(diào)用者釋放內(nèi)存甚侣。

緩存的解決方案

直接將內(nèi)存作為緩存的方案要十分慎重,除了限制緩存的大小间学,另外要考慮的事情是殷费,進(jìn)程之間無法共享內(nèi)存,如果在進(jìn)程內(nèi)使用緩存低葫,這些緩存不可避免地有重復(fù)详羡,對物理內(nèi)存的使用是一種浪費(fèi),如何使用大量緩存嘿悬,目前比較好的解決方案是采用進(jìn)程外的緩存实柠,進(jìn)程自身不存儲(chǔ)狀態(tài),外部緩存軟件有著良好的緩存過期淘汰策略以及自有的內(nèi)存管理善涨,不影響Node進(jìn)程的性能窒盐,它的好處多多,在Node中主要可以解決以下兩個(gè)問題钢拧。

1蟹漓、將緩存轉(zhuǎn)移到外部,減少常駐內(nèi)存的對象的數(shù)量源内,讓垃圾回收更高效葡粒。

2、進(jìn)程之間可以共享緩存膜钓。

{{% notice info %}}
其中較好的緩存有RedisMemcached
{{% /notice %}}

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

隊(duì)列在消費(fèi)者-生產(chǎn)者模型中經(jīng)常充當(dāng)中間產(chǎn)物嗽交,這是一個(gè)容易忽略的情況,因?yàn)榇蠖鄶?shù)應(yīng)用場景下颂斜,消費(fèi)的速度遠(yuǎn)遠(yuǎn)大于生產(chǎn)的速度夫壁,內(nèi)存泄漏不易產(chǎn)生,但是一旦消費(fèi)速度低于生產(chǎn)速度焚鲜,將會(huì)形成堆積掌唾。

比如:有的應(yīng)用會(huì)收集日志,如果欠缺考慮忿磅,也許會(huì)采用數(shù)據(jù)庫來記錄日志糯彬,日志通常是海量的,而數(shù)據(jù)庫的寫入效率遠(yuǎn)遠(yuǎn)低于文件直接寫入葱她,于是會(huì)造成數(shù)據(jù)庫寫入操作的堆積撩扒,而JavaScript相關(guān)的作用域也不會(huì)得到釋放,內(nèi)存占用不會(huì)回落,從而出現(xiàn)內(nèi)存泄漏搓谆。

表層的解決方案炒辉,是換用消費(fèi)速度更高的技術(shù),換用文件寫入日志的方式泉手,但是需要注意的是黔寇,如果生產(chǎn)速度因?yàn)槟承┰蛲蝗患ぴ觯笳呦M(fèi)速度因?yàn)橥蝗坏南到y(tǒng)故障降低斩萌,內(nèi)存泄漏還是可能出現(xiàn)的缝裤。

深度的解決方案應(yīng)該是控制隊(duì)列的長度,一旦堆積颊郎,應(yīng)當(dāng)通過監(jiān)控系統(tǒng)產(chǎn)生報(bào)警并通知相關(guān)人員憋飞,另一個(gè)解決方案是任意異步調(diào)用都應(yīng)該包含超時(shí)機(jī)制,一旦在限定時(shí)間內(nèi)未完成響應(yīng)姆吭,通過回調(diào)函數(shù)傳遞異常榛做,完成回落,使得任意異步調(diào)用都具備可控的響應(yīng)時(shí)間内狸,給消費(fèi)速度一個(gè)下限值检眯。

內(nèi)存泄漏排查

現(xiàn)在有許多工具用于定位Node的內(nèi)存泄漏,下面介紹其中的2種答倡,node-heapdump 和 node-memwatch

node-heapdump

安裝:npm install heapdump

使用:

var heapdump = require('heapdump'); // 引入
var http = require('http');
// 寫一份內(nèi)存泄漏的代碼
var leakArray = [];
var leak = function () {
  leakArray.push(new Array(2 * 1024 * 1024));
};
http.createServer(function (req, res) {
  leak();
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(1337);

上述代碼在使用node跑起來之后轰传,每次請求localhost:1337都會(huì)使得leakArray數(shù)組中的元素增加驴党,而得不到回收瘪撇,我們在多次訪問之后,leakArray的每一項(xiàng)都是較大的數(shù)組港庄,這時(shí)候輸入命令可以使用headdump抓拍一份堆內(nèi)存的快照倔既。

1、使用 lsof -i:1337 查看1337端口上的進(jìn)程的pid
2鹏氧、輸入以下命令抓取
kill -USR2 <pid>  // pid 進(jìn)程號(hào)

這份抓取的快照會(huì)在文件目錄下以heapdump-<sec>.<usec>.heapsnapshot的格式存放渤涌,這是一份較大的JSON文件,需要通過Chrome的開發(fā)者工具打開查看把还,開發(fā)者工具 - Memory - Profiles - 點(diǎn)擊右下角的load打開剛才的快照文件实蓬,就可以查看堆內(nèi)存中的詳細(xì)信息,可以查看內(nèi)存分布吊履,可以找出泄漏的數(shù)據(jù)安皱,然后根據(jù)這些信息找到泄漏的代碼。

heapdump.png

node-memwatch

安裝: npm install memwatch

使用:

var memwatch = require('memwatch');
var http = require('http');

memwatch.on('leak', function (info) {
  console.log('leak:');
  console.log(info);
});
memwatch.on('stats', function (stats) {
  console.log('stats:')
  console.log(stats);
});

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

http.createServer(function (req, res) {
  leak();
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(1337);
console.log('Server running at http://127.0.0.1:1337/');

在進(jìn)程使用node-memwatch之后艇炎,每次進(jìn)行全堆垃圾收集時(shí)酌伊,將會(huì)觸發(fā)一次stats事件,這個(gè)事件將會(huì)傳遞內(nèi)存的統(tǒng)計(jì)信息缀踪,在對上述代碼創(chuàng)建的服務(wù)進(jìn)程進(jìn)行訪問時(shí)居砖,某次stats事件打印的數(shù)據(jù)如下所示虹脯。

stats:
{ 
  num_full_gc: 4, // 第幾次全堆垃圾回收
  num_inc_gc: 23, // 第幾次增量垃圾回收
  heap_compactions: 4, // 第幾次對老生代進(jìn)行整理
  usage_trend: 0, // 使用趨勢
  estimated_base: 7152944, // 預(yù)估基數(shù)
  current_base: 7152944, // 當(dāng)前基數(shù)
  min: 6720776, // 最小
  max: 7152944  // 最大
}

如果經(jīng)過連續(xù)5次垃圾回收后,內(nèi)存仍然沒有得到釋放奏候,這意味著有內(nèi)存泄漏的產(chǎn)生循集,這時(shí)候node-memwatch會(huì)觸發(fā)一個(gè)leak事件,某次leak事件得到的數(shù)據(jù)如下:

leak:
{ 
  start: Mon Oct 07 2013 13:46:27 GMT+0800 (CST),
  end: Mon Oct 07 2013 13:54:40 GMT+0800 (CST),
  growth: 6222576,
  reason: 'heap growth over 5 consecutive GCs (8m 13s) - 43.33 mb/hr' 
}

這個(gè)數(shù)據(jù)能顯示5次垃圾回收的過程中內(nèi)存增長了多少蔗草,而具體問題產(chǎn)生在何處還需要從V8堆內(nèi)存上定位暇榴,node-memwatch提供了抓取快照和比較快照的功能,它能夠比較堆上對象的名稱和分配數(shù)量蕉世,從而找到導(dǎo)致內(nèi)存泄漏的元兇蔼紧。

var memwatch = require('memwatch');
var leakArray = [];

var leak = function () {
  leakArray.push("leak" + Math.random());
};
// 抓取第一次
var hd = new memwatch.HeapDiff();
for (var i = 0; i < 10000; i++) {
leak();
}
// 抓取第二次并進(jìn)行比較得出diff
var diff = hd.end();
console.log(JSON.stringify(diff, null, 2));

運(yùn)行以上代碼,得到j(luò)son字符串如下狠轻。

{
  "before": {
    "nodes": 11719,
    "time": "2013-10-07T06:32:07.000Z",
    "size_bytes": 1493304,
    "size": "1.42 mb"
  },
  "after": {
    "nodes": 31618,
    "time": "2013-10-07T06:32:07.000Z",
    "size_bytes": 2684864,
    "size": "2.56 mb"
  },
  "change": {
    "size_bytes": 1191560,
    "size": "1.14 mb",
    "freed_nodes": 129,
    "allocated_nodes": 20028,
    "details": [
      {
        "what": "Array",
        "size_bytes": 323720,
        "size": "316.13 kb",
        "+": 15,
        "-": 65
      },
      {
        "what": "Code",
        "size_bytes": -10944,
        "size": "-10.69 kb",
        "+": 8,
        "-": 28
      },
      {
        "what": "String",
        "size_bytes": 879424,
        "size": "858.81 kb",
        "+": 20001,
        "-": 1
      }
    ]
  }
}

其中change節(jié)點(diǎn)下的freed_nodes和allocated_nodes奸例,記錄了釋放的節(jié)點(diǎn)數(shù)量和分配的節(jié)點(diǎn)數(shù)量,由于內(nèi)存泄漏向楼,分配的節(jié)點(diǎn)數(shù)量遠(yuǎn)遠(yuǎn)多余釋放的節(jié)點(diǎn)數(shù)量查吊。

在detail數(shù)組可以看出每種類型的分配和釋放數(shù)量,+ 和 - 號(hào)分別表示分配和釋放的對象數(shù)量湖蜕,其中可以看出有大量的字符串沒有被回收逻卖。

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

由于Node的內(nèi)存限制,操作大文件也需要小心昭抒,好在Node提供了stream模塊用于處理大文件评也。

stream是Node原生模塊,繼承自EventEmitter灭返,具備基本的自定義事件功能盗迟,同時(shí)抽象出標(biāo)準(zhǔn)的事件和方法, 在Node中大多數(shù)模塊都有stream的應(yīng)用熙含,比如fs的createReadStream()和createWriteStream()方法可以分別用于創(chuàng)建文件的可讀流和可寫流罚缕,process模塊中stdin和stdout分別是可讀流和可寫流的示例。

由于V8的內(nèi)存限制怎静,我們無法通過fs.readFile()和fs.writeFile()直接進(jìn)行大文件的操作邮弹,而改用fs.createReadStream()和fs.createWriteStream()方法通過流的方式實(shí)現(xiàn)對大文件的操作。

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

reader.on('data', function (chunk) {
  writer.write(chunk);
});
reader.on('end', function () {
  writer.end();
});

// 或者簡寫為

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

可讀流提供了管道方法pipe()蚓聘,封裝了data事件和寫入操作腌乡,通過流的方式,上述代碼不會(huì)受到V8內(nèi)存限制的影響或粮,有效的提高了程序的健壯性导饲。

如果不需要進(jìn)行字符串層面的操作,則不需要借助V8來處理,可以嘗試進(jìn)行純粹的Buffer操作渣锦,這不會(huì)受到V8堆內(nèi)存的限制硝岗。但是需要注意的是,物理內(nèi)存仍然有限制袋毙。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末型檀,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子听盖,更是在濱河造成了極大的恐慌胀溺,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件皆看,死亡現(xiàn)場離奇詭異仓坞,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)腰吟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門无埃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人毛雇,你說我怎么就攤上這事嫉称。” “怎么了灵疮?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵织阅,是天一觀的道長。 經(jīng)常有香客問我震捣,道長荔棉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任伍派,我火速辦了婚禮江耀,結(jié)果婚禮上剩胁,老公的妹妹穿的比我還像新娘诉植。我一直安慰自己,他們只是感情好昵观,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布晾腔。 她就那樣靜靜地躺著,像睡著了一般啊犬。 火紅的嫁衣襯著肌膚如雪灼擂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天觉至,我揣著相機(jī)與錄音剔应,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛峻贮,可吹牛的內(nèi)容都是我干的席怪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼纤控,長吁一口氣:“原來是場噩夢啊……” “哼挂捻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起船万,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤刻撒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后耿导,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體声怔,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年舱呻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了捧搞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡狮荔,死狀恐怖胎撇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情殖氏,我是刑警寧澤晚树,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站雅采,受9級(jí)特大地震影響爵憎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜婚瓜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一宝鼓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧巴刻,春花似錦愚铡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至柠座,卻和暖如春邑雅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背妈经。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工淮野, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留捧书,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓骤星,卻偏偏與公主長得像鳄厌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子妈踊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

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