❝本人最近沉迷於Node的探索,也對Node用於後端開發感興趣。因此本着探索的精神,認真重讀了一下《深刻淺出Node.js》關於內存的部分,以爲本身其實開始不會去關心瞭解這些內容,如今真的開闊個人眼界,受益不淺。node
❞
隨着Node的發展,JavaScript的運行已經再也不侷限於運行在瀏覽器中了,Node在服務端的應用使得愈來愈多的問題也顯現出來。對於剛接觸JavaScript的開發者來講,基本上不多會想到內存分配或者內存泄露的問題,爲了跟上潮流,就講解一下Node是如何合理高效地使用內存,防止內存泄漏。web
JavaScript編程不用關心內存的分配和釋放的問題,由於它有垃圾回收機制來管理。可是咱們對於內存管理更深一步的瞭解可謂是不多,到底內存是怎麼分配的、垃圾回收是怎麼運行的?算法
咱們先來了解一下這二者是什麼關係。首先V8是谷歌開發出來的瀏覽器引擎,性能可謂是十分優異,使得JavaScript編寫服務器程序成爲可能。V8的由來是虛擬機專家Lars Bak開發出來的,開發者以前的工做一直都是專一於高性能的虛擬機開發。編程
Node是一個構建在Chrome的JavaScript運行時平臺,由此一來,Node的高性能就如同抱住了V8的大腿,能夠隨着V8的升級而享受更好的性能。後端
Node經過JavaScript使用內存的話會有限制,並且Node沒法直接操做大內存對象。64位系統下約爲1.4GB,而32位系統下約爲0.7GB。緣由是Node基於V8構建,它的內存分配和管理都是由V8來控制,雖然在瀏覽器下這種分配機制沒有問題,可是在Node中倒是有問題的。瀏覽器
在V8中,全部的JavaScript對象都是經過堆來分配的,對象佔用的內存空間大小是不肯定的。 服務器
當咱們在代碼裏寫上一些變量的時候,咱們就會向堆中的內存空間進行申請。咱們上面說過V8是有內存分配限制的,這是由於V8的垃圾回收機制影響。咱們試想一下假若有1.5GB的垃圾須要等待回收,而V8的垃圾回收機制運行一次須要每50毫秒以上,在這段時間內,程序會沒法運行。閉包
假如說我非要打開這個限制也不是不行,咱們能夠在Node的啓動時候更改一下參數便可:編輯器
node --max-old-space-size=1700 xxx.js //單位是MB
node --max-new-space-size=1024 xxx.js //單位是KB
複製代碼
上面代碼的意思就是新生代和老生代空間的更改,稍後解析什麼是新生代和老生代空間。函數
在V8中,咱們知道內存空間能夠分爲新生代和老生代。新生代空間主要是保存一些存活時間較短的對象,而老生代空間主要存儲一些存活時間較長的對象。
以前說過64位系統只能使用約1.4GB,32位只能使用0.7GB的空間。這空間是包含新生代和老生代空間,且老生代的空間比新生代要多。
V8主要採用兩種不一樣的算法,分別做用於新生代和老生代,由於二者的關係不同。主要有類算法,第一是Scavenge算法,第二是Mark-Sweep & Mark-Compact。新老生代的特色以下:
「新生代:存活對象較少」
「老生代:存活對象較多」
①Scavenge算法
這就完成了垃圾回收的一次清理過程。
②Mark-Sweep算法
因爲老生代空間中,存活的對象較多,使用 Scavenge 算法的話遍歷的時間會相對來講比較長,並且還會浪費一半的空間來存放對象,效率比較低。總的來講,Scavenge 算法複製存活的對象,而 Mark-Sweep 算法清除死亡的對象。
③Mark-Compact算法
咱們看到上面這種算法回收一次事後產生了零碎的內存空間「,假如這時候進來一個比較大的內存對象就沒法完成分配,提早觸發新一輪的垃圾回收機制。因此基於這個問題,在原來算法的基礎上,採用」壓縮的辦法,在標記活對象的過程當中,對象會往一邊移動。而後標記完成以後直接清除邊界的內存。
④Incremental Marking算法
與前面三種不同的是,這種算法**採用了「步進」的方式**進行,由於垃圾回收機制運行一次的時間也是有的,對於垃圾回收機制運行時間過長,那麼頁面的應用邏輯都要停下來等待,這種影響也是比較大的。
對於新生代來講不用擔憂這個問題,由於新生代存活對象比較少,內存佔用也比較小。可是對於老生代來講,「對象比較多也比較大」,停頓形成的影響大,因此必需要讓應用邏輯受垃圾回收的影響小一點。
所以採用「步進」的方式,垃圾回收機制和邏輯代碼**分段執行**,緩解停頓時間過長而致使應用邏輯執行不了的問題。
關於內存的話咱們就不得不聊一下做用域的問題了。咱們所寫的代碼中,能夠造成做用域的無非就是幾個方面,有函數、with以及全局做用域。
var A = function(){
var user = {}
};
A();
複製代碼
咱們在執行這個函數的時候就會建立一個函數做用域,也會建立一個局部的變量user。這個user只能在函數做用域裏面使用,函數執行完畢以後做用域銷燬,同時對象也失去引用,其引用的對象下次的垃圾回收時就會釋放。這裏的user是一個小對象,就會分配在新生代的from空間中。
咱們知道做用域會有一個做用域鏈的概念,就是在當前的做用域找不到變量以後就會向父級的做用域尋找,一直向外擴散,找不到就會拋出未定義的錯誤。
小結:
定時器
全局變量
,此種變量須要進程退出纔會被釋放,引用的對象就會在
常駐內存
中(
老生代
)
咱們能夠調用process.memoryUsage()查看Node進程的內存使用狀況。
heapTotal
(堆中總共申請的內存) 和 heapUsed
(堆中使用的內存) 表明 V8 的內存使用狀況。 external
表明 V8 管理的,綁定到 Javascript 的 C++ 對象的內存使用狀況。 rss
是常駐空間大小, 是給這個進程分配了多少物理內存(佔總分配內存的一部分),包含全部的 C++ 和 JavaScript 對象與代碼。
上面咱們說過老生代的內存空間
大概在1400MB
左右,爲了驗證一下,不惜代價搞了一波內存泄漏的操做,看一下到底內存是怎麼被消耗掉的。咱們手動構造了一個全局的對象,讓它放在常駐內存中,也就是老生代中。
function showMemory(j) {
console.log(`這是第${j+1}次運行`)
var mem = process.memoryUsage();
function format(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( "======================================================================================" ); } //吃人函數 function useMem() { 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 < 20; j++) {//運行20次,實際上也不會達到,由於會內存耗盡 showMemory(j); total.push(useMem()); } showMemory(j) 複製代碼
結果也是很是的amazing啊,運行不到10次內存就消耗完了。你們能夠想象一下假如你的內存有泄露的顯現是多麼可怕,最終就會致使整個進程的退出了。這裏咱們能夠清楚地看到,老生代的空間大概是1400MB左右,由於假如再執行一次就會超出範圍了。
上面咱們測試的是堆中的內存,可是咱們發現最後一次總共申請的內存只有1290.33MB,可是堆中卻佔據了1300.81MB,那麼多出來的那部分是哪裏的呢?沒錯,其實就是堆外內存。這部分的內存是不受V8控制的。
咱們改變一下上面的「吃人」函數:
function useMem() {
var size = 200 * 1024 * 1024;
var buffer = new Buffer(size);
for (var i = 0; i < size; i++) {
buffer[i] = 0;
}
return buffer;
}
複製代碼
咱們發現循環的21次都成功運行,並且咱們看到常駐內存那裏已經超過了V8的限制。這裏的buffer對象是不受V8控制和分配的,屬於堆外內存。
Node的內存主要是經過V8進行分配和Node自行分配的部分。可是受V8垃圾回收限制的主要是V8的堆內存。
Node將JavaScript的主要應用擴展到了服務器端,因此咱們考慮的細節也要和瀏覽器的不一樣,更多的是對內存資源的分配問題,稍有不慎可能會寫出一些內存泄漏的問題,使得垃圾回收機制不能清理釋放內存,嚴重可能會致使服務器崩潰。
因此平時咱們寫代碼也要謹慎一點,不要寫全局變量或者頻繁使用閉包,這些若是沒有正確釋放內存的話也會致使內存泄漏。內存泄漏的本質其實就是應當回收的對象可是卻沒有被回收(轉移到老生代空間)。
參考書籍: