詳解Nodejs內(nèi)存治理
s運(yùn)行的宿主環(huán)境不同,相應(yīng)的對(duì)內(nèi)存治理的要求也不一樣,當(dāng)宿主環(huán)境是瀏覽器時(shí),由于網(wǎng)頁(yè)的運(yùn)行時(shí)間短,且只運(yùn)行在用戶的機(jī)器上(相當(dāng)于分布式),即使內(nèi)存使用過多或者存在一定的內(nèi)存泄漏,也并不會(huì)對(duì)終端用戶產(chǎn)生太大的影響。當(dāng)宿主環(huán)境編程服務(wù)器(Node)時(shí),情況就大不相同了,本身代碼運(yùn)行在固定的幾臺(tái)機(jī)器(集中式)上,而且運(yùn)行的時(shí)間是長(zhǎng)時(shí)間運(yùn)行,一旦內(nèi)存治理不好出現(xiàn)了內(nèi)存膨脹甚至是內(nèi)存泄漏的情況的話,就會(huì)出現(xiàn)服務(wù)器端響應(yīng)時(shí)間變長(zhǎng)甚至是服務(wù)crash的情況。
Nodejs是基于V8構(gòu)建的,所以在Node中使用的JavaScript對(duì)象基本上(Buffer就不是)都是通過V8來進(jìn)行分配和管理的。V8在占用內(nèi)存大小上做了限制(64位操作系統(tǒng),單個(gè)Node進(jìn)程可使用的最大堆內(nèi)存大小約為1.5GB)。即使服務(wù)器的內(nèi)存很大,但是由于V8的這種限制,導(dǎo)致Node無(wú)法充分利用服務(wù)器的資源。即便如此,為什么V8要做這樣的限制呢?做這樣限制的原因其實(shí)是與垃圾回收機(jī)制相關(guān),以1.5GB的垃圾回收堆內(nèi)存堆為例,V8做一次小的垃圾回收需要50ms以上,做一次全量的垃圾回收甚至要1s以上,要知道垃圾回收過程中JavaScript線程是要處于暫停執(zhí)行的狀態(tài),太長(zhǎng)的暫行時(shí)間對(duì)于后端服務(wù)的性能是會(huì)產(chǎn)生較大影響的,所以出于這方面考慮,V8對(duì)堆內(nèi)存做了限制。即便如此,V8還是提供了可以自定義堆內(nèi)存大小的方式(--max-old-pace-size),old-space代表老生代、new-space代表新生代。
node --max-old-space-size=xxx index.js //單位為MB // 之前還可以通過-max-new-space-size來定義新生代堆大小,現(xiàn)在已經(jīng)不可以了
當(dāng)由于內(nèi)存泄漏導(dǎo)致服務(wù)器一直頻繁重啟的時(shí)候,建議先調(diào)大堆內(nèi)存大小來為定位問題爭(zhēng)取時(shí)間,畢竟服務(wù)響應(yīng)慢總比直接返回錯(cuò)誤頁(yè)對(duì)于用戶而言會(huì)更好接受一點(diǎn)。
為什么需要老生代和新生代?
老生代和新生代其實(shí)是分代式垃圾回收機(jī)制里面的不同的分代,因?yàn)闆]有一種垃圾回收算法能夠勝任所有的場(chǎng)景,不同的對(duì)象生存周期其實(shí)需要不同的回收策略才能達(dá)到最好的效果,所以V8采用分代式垃圾回收機(jī)制,降對(duì)象按的存活時(shí)間進(jìn)行不同的分代,然后對(duì)不同分代(新生代、老生代)的內(nèi)存施以更適合也更好的算法。
新生代中的對(duì)象存活時(shí)間較短,而老生代中的對(duì)象存活時(shí)間較長(zhǎng)甚至是常駐內(nèi)存?;诖怂栽O(shè)計(jì)的新生代的內(nèi)存普遍要比老生代的內(nèi)存小很多,V8中新生代最大內(nèi)存是32M(64位系統(tǒng)為例),老生代最大內(nèi)存是1400MB。V8實(shí)際使用的堆內(nèi)存大小是新生代+老生代所用內(nèi)存之和(1432MB),但是V8最大值其實(shí)是比使用的內(nèi)存對(duì)大小額外大了32M(1464MB)
新生代如何做垃圾回收?
新生代的采用名叫Scavenge的垃圾回收算法。在Scavenge的具體實(shí)現(xiàn)中,主要采用了Cheney算法,Cheney算法通過將新生代堆一分為二,一個(gè)使用(From semispace),一個(gè)空閑(To semispace)。創(chuàng)建對(duì)象的時(shí)候,現(xiàn)在From空間中進(jìn)行分配,當(dāng)需要進(jìn)行垃圾回收時(shí),就檢查From空間中的存活對(duì)象,然后將存活的對(duì)象拷貝到To空間,同時(shí)清空From空間,并將From和To互換,整個(gè)垃圾回收過程中就是將存活對(duì)象在兩個(gè)seispace之間進(jìn)行復(fù)制。對(duì)于生命周期短的場(chǎng)景存活對(duì)象在整個(gè)對(duì)象中占比較小,所以Scavenge采用的是復(fù)制存活的對(duì)象,但是Scavenge只能利用堆內(nèi)存一半的空間,這是典型的用空間換時(shí)間的體現(xiàn)。
當(dāng)一個(gè)對(duì)象經(jīng)過多次垃圾回收依然存活的話,就會(huì)被認(rèn)為是生命周期較長(zhǎng)的對(duì)象,一方面新生代堆比較小,另一方面重復(fù)復(fù)制生命周期長(zhǎng)的對(duì)象也很沒有效率,所以對(duì)于生命周期長(zhǎng)的對(duì)象會(huì)被移到老生代中去。新生代對(duì)象移動(dòng)到老生代有兩個(gè)對(duì)象:1.對(duì)象是否是生命周期較長(zhǎng)的對(duì)象(已經(jīng)經(jīng)歷過垃圾回收)2.To空間使用占比是否超過了25%。限制25%的原因是由于垃圾回收完成后To會(huì)變成From,如果不做限制的話可能會(huì)出現(xiàn)From很快被用光的情況,出現(xiàn)頻繁的垃圾回收,也會(huì)影響效率。
老生代如何做垃圾回收?
老生代由于存活對(duì)象占較大比重,不適合對(duì)存活對(duì)象進(jìn)行操作,使用Scavenge算法就不太合適了,因此老生代采用了Mark-Sweep和Mark-Compact相結(jié)合的方式。
Mark-Sweep分為標(biāo)記和清除兩個(gè)階段,在標(biāo)記階段遍歷堆中所有對(duì)象,標(biāo)記活著的對(duì)象,然后在清除階段未被標(biāo)記的對(duì)象將會(huì)被清除掉。Mark-Sweep解決了內(nèi)存釋放的問題但是由于沒有像Scavenge那樣復(fù)制對(duì)象的操作導(dǎo)致內(nèi)存碎片化不連續(xù)。而Mark-Compact就是用來解決內(nèi)存碎片化問題的。Mark-Compact會(huì)將存活的對(duì)象往一端移動(dòng),移動(dòng)完成后直接清理掉邊界外的內(nèi)存,這樣就有大段的連續(xù)可用內(nèi)存了,但是由于涉及到對(duì)象的移動(dòng),因此Mark-Compact的速度要比Mark-Sweep慢了。V8主要使用Mark-Sweep,只有當(dāng)空間不足以對(duì)新生代中今生過來的對(duì)象進(jìn)行分配時(shí)才使用Mark-Compact。
垃圾回收過程中會(huì)導(dǎo)致應(yīng)用程序暫停執(zhí)行,由于新生代本身空間較小,且要復(fù)制的存活對(duì)象占比也少,因此即便執(zhí)行全量垃圾回收也影響不大,但是老生代空間很大,存活對(duì)象也多,執(zhí)行一次全量垃圾回收對(duì)于應(yīng)用程序暫停會(huì)是一個(gè)比較長(zhǎng)的時(shí)間,因此V8將老生的標(biāo)記改成了增量更新的方式,使得標(biāo)記和應(yīng)用程序交替執(zhí)行直到標(biāo)記完成,然后垃圾回收再執(zhí)行后面的清理工作。注意清理工作并不是增量的。
開發(fā)者可以指定強(qiáng)制垃圾回收嗎?
答案是可以了,在啟動(dòng)node服務(wù)的時(shí)候使用--expose-gc flag
$ node --expose-gc file.js
這樣全局對(duì)象上就有了執(zhí)行垃圾回收的函數(shù)
global.gc();
推薦更安全的寫法
function forceGC() if (global.gc) { global.gc(); } else { console.warn('No GC hook! Start your program as `node --expose-gc file.js`.'); } }
最后給大家分享一下參考資料:
https://speakerdeck.com/addyosmani/javascript-memory-management-masterclass
http://chabaoo.cn/article/140005.htm
https://www.xarg.org/2016/06/forcing-garbage-collection-in-node-js-and-javascript/
相關(guān)文章
nodejs使用readline逐行讀取和寫入文件的實(shí)現(xiàn)
這篇文章給大家介紹了nodejs使用readline逐行讀取和寫入文件的實(shí)現(xiàn)方法,文中通過代碼示例給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-01-01Nodejs 發(fā)布自己的npm包并制作成命令行工具的實(shí)例講解
今天小編就為大家分享一篇Nodejs 發(fā)布自己的npm包并制作成命令行工具的實(shí)例講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-05-05寶塔部署nodejs項(xiàng)目的實(shí)戰(zhàn)步驟
前段時(shí)間部署node項(xiàng)目的時(shí)候出現(xiàn)了一點(diǎn)問題,所以想著給大家總結(jié)下,這篇文章主要給大家介紹了關(guān)于寶塔部署nodejs項(xiàng)目的實(shí)戰(zhàn)步驟,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12基于NodeJS開發(fā)釘釘回調(diào)接口實(shí)現(xiàn)AES-CBC加解密
這篇文章主要介紹了基于NodeJS開發(fā)釘釘回調(diào)接口 實(shí)現(xiàn)AES-CBC加解密,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08