V8內(nèi)存限制
Node與其他語言不同的一個地方瞄桨,就是其限制了JavaScript所能使用的內(nèi)存(64位為1.4GB,32位為0.7GB)凰荚,這也就意味著將無法直接操作一些大內(nèi)存對象资锰。這很令人匪夷所思,因為很少有其他語言會限制內(nèi)存的使用快压。
原因?
V8之所以限制了內(nèi)存的大小础锐,表面上的原因是V8最初是作為瀏覽器的JavaScript引擎而設計嗓节,不太可能遇到大量內(nèi)存的場景荧缘。
而深層次的原因則是由于V8的垃圾回收機制的限制皆警。由于V8需要保證JavaScript應用邏輯與垃圾回收器所看到的不一樣,V8在執(zhí)行垃圾回收時會阻塞JavaScript應用邏輯截粗,直到垃圾回收結束再重新執(zhí)行JavaScript應用邏輯信姓,這種行為被稱為“全停頓”(stop-the-world)。
若V8的堆內(nèi)存為1.5GB绸罗,V8做一次小的垃圾回收需要50ms以上意推,做一次非增量式的垃圾回收甚至要1秒以上。這樣瀏覽器將在1s內(nèi)失去對用戶的響應珊蟀,造成假死現(xiàn)象菊值。如果有動畫效果的話,動畫的展現(xiàn)也將顯著受到影響育灸。因此當時的考慮下腻窒,限制內(nèi)存是最好的選擇。
突破限制磅崭?
當然這個限制是可以打開的儿子,類似于JVM,我們通過在啟動node時可以傳遞--max-old-space-size或--max-new-space-size來調(diào)整內(nèi)存限制的大小砸喻,前者確定老生代的大小柔逼,單位為MB,后者確定新生代的大小割岛,單位為KB愉适。這些配置只在V8初始化時生效,一旦生效不能再改變癣漆。
V8對象分配
在V8中所有js對象通過堆進行分配维咸。node中提供了V8中內(nèi)存的使用量查看方式,執(zhí)行下面代碼:
查看node內(nèi)存使用情況
使用process.memoryUsage()
,除此之外os模塊中的totalmen()和freemen()方法也能查看內(nèi)存使用情況,不過這個是查看操作系統(tǒng)的內(nèi)存使用情況腰湾。
node
> process.memoryUsage()
{ rss: 24633344, heapTotal: 10522624, heapUsed: 5105552 }
//單位字節(jié)23MB/ 4MB/ 10MB
//rss:resident set size的縮寫雷恃,表示進程的常駐內(nèi)存部分。進程的內(nèi)存總共有幾部分费坊,一部分是rss,其余部分在交換區(qū)(swap)或者文件系統(tǒng)(filesystem)中倒槐。
除了rss外,heapTotal和headUsed對應的是V8的堆內(nèi)存信息附井,前者是堆中總共申請的內(nèi)存量讨越,后者表示目前堆中使用中的內(nèi)存量。單位都是字節(jié)永毅。
附加查看操作系統(tǒng)內(nèi)存使用情況
node
>os.totalmen()
858994592
>os.freemen()
4527833088
//單位字節(jié) 內(nèi)存8G,剩余4.2G左右
V8內(nèi)存分配基礎
在V8中所有的JavaScript對象都是通過堆來分配的把跨。為了提高垃圾回收的效率恒傻,V8將堆分為新生代和老生代兩個部分导帝,其中新生代為存活時間較短的對象(需要經(jīng)常進行垃圾回收)夺刑,而老生代為存活時間較長的對象(垃圾回收的頻率較低)秸侣。
新生代和老生代的默認內(nèi)存限制在啟動的時候就確定了跳仿,沒辦法根據(jù)應用使用內(nèi)存情況自動擴充傍妒,當應用分配過多內(nèi)存時涮因,就會引起OOM(Out Of Memory奸汇,內(nèi)存溢出)進程錯誤县钥。64位系統(tǒng)和32位系統(tǒng)的內(nèi)存限制不同秀姐,分別如下:
在node啟動時,通過--max-new-space-size和--max-old-space-size可分別設置新生代和老生代的默認內(nèi)存限制
V8垃圾回收原理
1.常用垃圾回收基本算法
2.V8的分代垃圾回收
V8垃圾回收策略主要基于分代式垃圾回收機制若贮。
上面提到過省有,V8將內(nèi)存分為新生代和老生代,新生代中對象存活時間較短谴麦,老生代中對象存活時間較長蠢沿。為了最大程度的提升垃圾回收效率,V8使用了一種綜合性的方法细移,其在新生代和老生代中分別使用上文提到的不同的基本垃圾回收算法搏予。
2.1 新生代垃圾回收算法Scavenge
在新生代中,由于內(nèi)存較小(64位系統(tǒng)為64MB)且存活對象較少弧轧,V8采取了一種以空間換時間的方案雪侥,即停止-復制算法 (Stop-Copy)。它將新生代分為兩個半?yún)^(qū)域(semi-space)精绎,分別稱為from空間和to空間速缨。一次垃圾回收分為兩步:
(1) 將from空間中的活對象復制到to空間
(2) 切換from和to空間
V8將新生代中的一次垃圾回收過程,稱為Scavenge代乃。
2.2老生代垃圾回收算法
老生代的內(nèi)存空間較大且存活對象較多旬牲,因此其垃圾回收算法也就沒有新生代那么簡單了仿粹。為此V8使用了標記-清除算法 (Mark-Sweep)進行垃圾回收,并使用標記-壓縮算法 (Mark-Compact)整理內(nèi)存碎片原茅,提高內(nèi)存的利用率吭历。老生代的垃圾回收算法步驟如下:
(1).對老生代進行第一遍掃描,標記存活的對象
(2).對老生代進行第二次掃描擂橘,清除未被標記的對象
(3).將存活對象往內(nèi)存的一端移動
(4).清除掉存活對象邊界外的內(nèi)存
從上面的表格可以看出晌区,停止-復制(Stop-Copy)、標記-清除(Mark-Sweep)和標記-壓縮(Mark-Compact)都需要停止應用邏輯通贞,我們將之稱為stop-the-world朗若。但因為新生代內(nèi)存較小且存活對象較少,即便stop-the-world昌罩,對應用的性能影響也不大哭懈;而老生代的內(nèi)存很大,stop-the-world就不能接受了茎用,為此V8引入了增量標記遣总。增量標記使得應用邏輯和垃圾回收交替運行,減少了垃圾回收對應用邏輯的干擾绘搞。
2.3 分代垃圾回收的代價
在討論新生代中的垃圾回收算法Scavenge時彤避,我們忽略了許多細節(jié)。
真的僅僅掃描新生代的內(nèi)存空間夯辖,就能確定新生代的活動對象嗎?
當然不是,老生代的對象也可能引用新生代的對象啊董饰。如果每次運行Scavenge算法時蒿褂,都要掃描老生代空間的話,這種操作帶來的性能損耗就完全抵消了分代式垃圾回收所帶來的性能提升卒暂。為此V8使用寫屏障技術解決了這個問題:
V8使用一個列表(我們稱之為CrossRefList)記錄所有老生代對象指向新生代的情況啄栓,當有老生代中的對象出現(xiàn)指向新生代對象的指針時,便記錄下來這樣的跨區(qū)指向也祠。由于這種記錄行為總是發(fā)生在寫操作時昙楚,因此被稱為寫屏障。
每個寫操作都要經(jīng)歷這樣一關诈嘿,性能上必然有損失堪旧,這是分代垃圾回收的代價之一。通過使用寫屏障技術奖亚,我們在對新生代進行垃圾回收時淳梦,只需要掃描新生代From空間和CrossRefList列表就可以確定活動對象了。
垃圾回收監(jiān)控
理解了垃圾回收的基本原理以后昔字,我們來看一看如何監(jiān)控node的垃圾回收情況爆袍。查看垃圾回收方式的最方便的方法是通過在啟動時使用--trace-gc參數(shù):
node --trace-gc app.js
//可以自己試試
而一種更加程序化的方式是使用memwatch-next模塊,該模塊在node每一次進行全量垃圾(full-gc,包括標記-清除和標記-壓縮)回收時觸發(fā)相應的事件:
var memwatch = require('memwatch-next');
memwatch.on('stats', function(stats) {
console.log(stats);
});
上述代碼監(jiān)控每一次全量垃圾回收動作,并打印出相應垃圾回收統(tǒng)計信息:
{
"num_full_gc": 8, //目前為止進行全量GC的次數(shù)
"num_inc_gc": 18, //目前為止進行增量GC的次數(shù)
"heap_compactions": 8, //目前為止進行的內(nèi)存壓縮的次數(shù)
"usage_trend": 0, //內(nèi)存增長趨勢陨囊,如果一直大于0弦疮,則可能有內(nèi)存泄露
"estimated_base": 2592568,
"current_base": 2592568,
"min": 2499912,
"max": 2592568
}
內(nèi)存泄露原因
Node對內(nèi)存泄露十分敏感,哪怕一個字節(jié)的內(nèi)存泄露也會造成堆積蜘醋,垃圾回收過程中將會消耗更多的時間進行對象的掃描挂捅,應用響應速度變慢,直到進程內(nèi)存溢出堂湖,應用崩潰闲先。
盡管內(nèi)存泄露的情況不盡相同,但其實實質(zhì)只有一個无蜂,那就是應當回收的對象出現(xiàn)意外沒有被回收伺糠,變成了常駐在老生代中的對象。
通常造成內(nèi)存泄露的原因:
- 緩存
- 隊列消費不及時
- 作用域未釋放
內(nèi)存泄露定位
使用上文提到的垃圾回收監(jiān)控方法斥季,我們可以知道程序是否有內(nèi)存泄露训桶,那么具體在什么地方有內(nèi)存泄露呢?我們需要借助于新的工具酣倾。node-heapdump提供了v8的堆內(nèi)存快照抓取工具舵揭。
1. 抓取對內(nèi)存鏡像
我們可以在程序中直接通過它提供的函數(shù)抓取內(nèi)存快照:
var heapdump = require('heapdump');
heapdump.writeSnapshot('/tmp/' + Date.now() + '.heapsnapshot');
在linux下,我們還可以通過向node進程發(fā)送信號來抓取內(nèi)存快照:
kill -USR2 pid
有了內(nèi)存快照后躁锡,我們就可以借助chrome的Profile工具午绳,具體的分析內(nèi)存泄露發(fā)生在什么地方了。
2. 三次快照法
利用chrome的Profile工具分析內(nèi)存泄露的經(jīng)典方法是三次快照法映之,我們需要首選準備3個內(nèi)存快照文件:
(1) 第一次獲取正常情況下內(nèi)存快照
(2) 第二次獲取發(fā)生內(nèi)存泄露時的內(nèi)存快照
(3) 第三次獲取繼續(xù)發(fā)生內(nèi)存泄露時的內(nèi)存快照
三次快照要求第一次必須在沒有出現(xiàn)內(nèi)存泄露時拦焚,是為了過濾一些無用的信息,使得分析結果可讀性更強杠输。