一、NodeJs執行環境
我們都知道在chrome瀏覽器內JavaScript是在v8上解釋執行的,NodeJs就是基於JavaScript環境和語言編寫的,它的執行環境也就離不開v8了。放張圖來說明一下瀏覽器和NodeJs執行環境的對比。
上圖可用看到,在瀏覽器中因爲需要渲染UI界面(HTML,CSS),所以多出來了瀏覽器的WebKit內核以及底層也會調用到顯卡來顯示渲染界面。
而NodeJs的使用場景多是在服務器端,也就是說沒有UI界面需要渲染,所以也就不在需要WebKit內核,底層相應的也就不會去調用到顯卡驅動。NodeJs的中間層libuv是其支持跨平臺運行的關鍵所在。
上圖也充分說明了在Chrome瀏覽器和NodeJs中JavaScript的執行環境都是V8,今天我們要說的重點也就是V8虛擬機的垃圾回收機制。
二、V8的內存限制
在其它後端語言中,例如java在使用內存時就沒限制(可用充分使用物理內存)。但是在JavaScript中,內存的使用就受到了V8虛擬機的限制。因爲V8是爲Chrome瀏覽器研發產生的,在瀏覽器環境內並不會存在使用大量內存的情況,所以V8在內存使用上對不同平臺做了相對應的限制:32位操作系統約爲0.7GB,64位操作系統約爲1.4GB。這也就意味着當我們在操作大文件時(超過該內存限制的文件),無法將文件全部讀取到內存中來,相對應的計算機的內存資源也就無法得到充分的使用。
三、V8對象分配(堆內存)
瞭解JVM的人都知道,java中的對象分配是在堆內存中的。V8也不例外,當我們在代碼中聲明變量並賦值時,所使用對象的內存就分配在堆中。如果已申請的堆空閒內存不夠分配新對象時,將繼續申請堆內存,直到堆的大小超過V8的限制爲止。
查看堆內存使用情況:
$ node
> process.memoryUsage();
{ rss: 20774912,
heapTotal: 7258112,
heapUsed: 4225360,
external: 9327 }
以上內存限制在Node中也是可用打破的,在啓動項目時指定堆內存大小,如下所示:
node --max-old-space-size=2048 index.js // 設置老齡代堆內存大小,單位MB
// 或者
node --max-new-space-size=1024 index.js // 設置新生代堆內存大小,單位MB
以上參數在V8初始化時生效,一旦生效就不能再動態修改。
四、V8的垃圾回收機制
V8的垃圾回收策略主要基於分代垃圾回收機制。在分代回收的基礎上,針對新生代和老齡代在其內部又做了相對應的回收算法:新生代內使用的是複製算法、老齡代內使用的是標記-清除算法以及標記-合併算法。
1、內存堆內存分配-分代算法
將整個堆內存分爲新生代和老齡代,新生代內保存的是最新創建且存活週期短的對象。老齡代內保存的是較爲活躍且存活週期長的對象。
2、複製算法(新生代內存)
該算法將內存一分爲二,每一部分空間稱爲semispace。在這兩個semispace空間中,只有一個處於使用中,另一個處於閒置狀態。正在使用的semispace空間我們稱之爲From空間,處於閒置的semispace空間我們稱之爲To空間。當我們分配對象時,先是在From空間進行分配。當新生代內存中開始垃圾回收時,會檢查From空間中的存活對象,這些存活對象將被複制到To空間中,而非存活對象佔用的空間將被釋放。完成複製後,From空間和To空間的角色發生互換,From空間變爲To空間,To空間變爲From空間。簡而言之,在新生代的垃圾回收過程中,就是通過將存活對象在兩個semispace空間之間進行復制。如下圖所示:
複製算法的缺點是隻能使用新生代內存中的一半內存空間大小。但複製算法由於只複製存活的對象,並且對於生命週期短的場景存活對象只佔少部分,所以它在時間效率上有優異的表現。
由於複製算法是典型的犧牲空間換取時間的算法,所以無法大規模應用在所有的垃圾回收中。但可以發現,複製算法非常適合應用在新生代中,因爲新生代中對象的生命週期較短,恰恰適合這個算法。
由上圖也可以看出,實際在V8中處於使用的內存是新生代一半的內存加上老齡代內存,有一半的新生代內存處於閒置狀態。
我們瞭解了新生代中的內存空間使用,那什麼樣的對象會從新生代中晉升到老齡代內存中呢?當一個對象經過多次複製依然存活時,它將會被認爲是生命週期較長的對象。這種較長生命週期的對象隨後會被移動到老齡代內存中,採用老齡代中新的算法進行管理。
對象從新生代晉升到老齡代的條件主要有兩個:一個是對象是否經歷過複製算法回收,一個是To空間的內存佔用比超過一定的限制。
3、標記-清除算法(老齡代內存)
標記清除算法分爲兩個階段,一階段是標記,另一階段是清除。與複製算法相比,標記-清除算法並不會將內存空間劃分爲兩半,所以不存在浪費一半空間的行爲。與複製算法不同的是,標記-清除算法在標記階段遍歷堆中的所有對象,並標記活着的對象,在隨後的清除階段中,只清除沒有被標記的對象。可以看出複製算法只複製活着的對象,而標記-清除算法只清理死亡的對象。這是因爲活對象在新生代中佔較小部分,死對象在老齡代中佔較小部分,這也是兩種回收方式能高效工作的原因。標記-清除算法的工作圖解如下:
標記-清除算法最大的問題是在進行一次標記清除回收後,內存空間會出現不連續的狀態。這些內存碎片會對後續的內存分配造成問題,因爲很可能出現需要分配一個大內存對象的情況,這時無法分配的情況下就會再次觸發垃圾回收,而這次的回收是不必要的,這時就該下面的回收算法登場了:標記-合併算法。
4、標記-合併算法(老齡代內存)
上面也說到了標記-清除算法存在的弊端,標記-合併算法就是爲了解決這些弊端而設計演變出來的。它們的差別在於對象在標記爲死亡後,在整理的過程中,將活着的對象往一端移動,在移動完成後,直接清理掉另一端死亡的對象。完成移動並清理完另一端死亡對象的內存後,老齡代內存空間就是連續的未使用和以使用了,這樣就可以進行大內存對象的分配了。
5、垃圾回收算法比較
回收算法 | 複製算法 | 標記-清除算法 | 標記-合併算法 |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空間開銷 | 少(有碎片) | 少(無碎片) | 雙倍空間(無碎片) |
是否移動對象 | 否 | 是 | 是 |