5. node-內(nèi)存控制-垃圾回收

內(nèi)存控制學習腦圖

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)常進行垃圾回收)夺刑,而老生代為存活時間較長的對象(垃圾回收的頻率較低)秸侣。


image.png

新生代和老生代的默認內(nèi)存限制在啟動的時候就確定了跳仿,沒辦法根據(jù)應用使用內(nèi)存情況自動擴充傍妒,當應用分配過多內(nèi)存時涮因,就會引起OOM(Out Of Memory奸汇,內(nèi)存溢出)進程錯誤县钥。64位系統(tǒng)和32位系統(tǒng)的內(nèi)存限制不同秀姐,分別如下:


image.png

在node啟動時,通過--max-new-space-size和--max-old-space-size可分別設置新生代和老生代的默認內(nèi)存限制

V8垃圾回收原理

1.常用垃圾回收基本算法

image.png

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)存

image.png

從上面的表格可以看出晌区,停止-復制(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ā)生在寫操作時昙楚,因此被稱為寫屏障。


image.png

每個寫操作都要經(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)存泄露時拦焚,是為了過濾一些無用的信息,使得分析結果可讀性更強杠输。


最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赎败,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蠢甲,更是在濱河造成了極大的恐慌僵刮,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鹦牛,死亡現(xiàn)場離奇詭異搞糕,居然都是意外死亡,警方通過查閱死者的電腦和手機能岩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門寞宫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拉鹃,你說我怎么就攤上這事辈赋■耆蹋” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵钥屈,是天一觀的道長悟民。 經(jīng)常有香客問我,道長篷就,這世上最難降的妖魔是什么射亏? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮竭业,結果婚禮上智润,老公的妹妹穿的比我還像新娘。我一直安慰自己未辆,他們只是感情好窟绷,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著咐柜,像睡著了一般兼蜈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拙友,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天为狸,我揣著相機與錄音,去河邊找鬼遗契。 笑死辐棒,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的姊途。 我是一名探鬼主播涉瘾,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼捷兰!你這毒婦竟也來了?” 一聲冷哼從身側響起负敏,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤贡茅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后其做,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體顶考,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年妖泄,在試婚紗的時候發(fā)現(xiàn)自己被綠了驹沿。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡蹈胡,死狀恐怖渊季,靈堂內(nèi)的尸體忽然破棺而出朋蔫,到底是詐尸還是另有隱情,我是刑警寧澤却汉,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布驯妄,位于F島的核電站,受9級特大地震影響合砂,放射性物質(zhì)發(fā)生泄漏青扔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一翩伪、第九天 我趴在偏房一處隱蔽的房頂上張望微猖。 院中可真熱鬧,春花似錦缘屹、人聲如沸凛剥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽当悔。三九已至,卻和暖如春踢代,著一層夾襖步出監(jiān)牢的瞬間盲憎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工胳挎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留饼疙,地道東北人。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓慕爬,卻偏偏與公主長得像窑眯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子医窿,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

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