關於Node.js中內存管理的思考與實踐

本人最近非常癡迷於Node的探索,也對Node用於後端開發感興趣。所以本着探索的精神,認真重讀了一下《深入淺出Node.js》關於內存的部分,覺得自己其實開始不會去關心瞭解這些內容,現在真的開闊我的眼界,受益匪淺。

歡迎大家關注我的公衆號《前端小時》,文章同步發送!

在這裏插入圖片描述

01 前言

隨着Node的發展,JavaScript的運行已經不再侷限於運行在瀏覽器中了,Node在服務端的應用使得越來越多的問題也顯現出來。對於剛接觸JavaScript的開發者來說,基本上很少會想到內存分配或者內存泄露的問題,爲了跟上潮流,就講解一下Node是如何合理高效地使用內存,防止內存泄漏。

02 V8垃圾回收與內存限制

JavaScript編程不用關心內存的分配和釋放的問題,因爲它有垃圾回收機制來管理。但是我們對於內存管理更深一步的瞭解可謂是很少,到底內存是怎麼分配的、垃圾回收是怎麼運行的?

01 Node與V8

我們先來了解一下這兩者是什麼關係。首先V8是谷歌開發出來的瀏覽器引擎,性能可謂是十分優異,使得JavaScript編寫服務器程序成爲可能。V8的由來是虛擬機專家Lars Bak開發出來的,開發者之前的工作一直都是專注於高性能的虛擬機開發。

Node是一個構建在Chrome的JavaScript運行時平臺,由此一來,Node的高性能就如同抱住了V8的大腿,可以隨着V8的升級而享受更好的性能。

02 V8的內存限制

Node通過JavaScript使用內存的話會有限制,而且Node無法直接操作大內存對象。64位系統下約爲1.4GB,而32位系統下約爲0.7GB。原因是Node基於V8構建,它的內存分配和管理都是由V8來控制,雖然在瀏覽器下這種分配機制沒有問題,但是在Node中卻是有問題的。

03 V8的對象分配

在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

上面代碼的意思就是新生代和老生代空間的更改,稍後解析什麼是新生代和老生代空間。

04 V8垃圾回收機制

在V8中,我們知道內存空間可以分爲新生代和老生代。新生代空間主要是保存一些存活時間較短的對象,而老生代空間主要存儲一些存活時間較長的對象。

之前說過64位系統只能使用約1.4GB,32位只能使用0.7GB的空間。這空間是包含新生代和老生代空間,且老生代的空間比新生代要多。
在這裏插入圖片描述

32位 64位
新生代 16MB 32MB
老生代 700MB 1400MB

V8主要採用兩種不同的算法,分別作用於新生代和老生代,因爲兩者的關係不一樣。主要有類算法,第一是Scavenge算法,第二是Mark-Sweep & Mark-Compact。新老生代的特點如下:

新生代:存活對象較少

老生代:存活對象較多

①Scavenge算法

  • 把新生代空間一分爲二,爲From空間和To空間
  • 內存先分配到From空間,垃圾回收會檢查From空間存活情況
  • 把From空間的存活對象移到To空間,釋放From空間
  • 把From和To空間對換

這就完成了垃圾回收的一次清理過程。
在這裏插入圖片描述

②Mark-Sweep算法

  • 遍歷堆中的所有對象,標記存活的對象
  • 清除沒有被標記的對象
    在這裏插入圖片描述

由於老生代空間中,存活的對象較多,使用 Scavenge 算法的話遍歷的時間會相對來說比較長,而且還會浪費一半的空間來存放對象,效率比較低。總的來說,Scavenge 算法複製存活的對象,而 Mark-Sweep 算法清除死亡的對象。

③Mark-Compact算法

我們看到上面這種算法回收一次過後**產生了零碎的內存空間,假如這時候進來一個比較大的內存對象就無法完成分配,提前觸發新一輪的垃圾回收機制。所以基於這個問題,在原來算法的基礎上,採用壓縮**的辦法,在標記活對象的過程中,對象會往一邊移動。然後標記完成之後直接清除邊界的內存。
在這裏插入圖片描述

④Incremental Marking算法

與前面三種不一樣的是,這種算法**採用了“步進”的方式**進行,因爲垃圾回收機制運行一次的時間也是有的,對於垃圾回收機制運行時間過長,那麼頁面的應用邏輯都要停下來等待,這種影響也是比較大的。

對於新生代來說不用擔心這個問題,因爲新生代存活對象比較少,內存佔用也比較小。但是對於老生代來說,對象比較多也比較大,停頓造成的影響大,所以必須要讓應用邏輯受垃圾回收的影響小一點。

因此採用“步進”的方式,垃圾回收機制和邏輯代碼**分段執行**,緩解停頓時間過長而導致應用邏輯執行不了的問題。
在這裏插入圖片描述

03 內存的使用

關於內存的話我們就不得不聊一下作用域的問題了。我們所寫的代碼中,可以形成作用域的無非就是幾個方面,有函數、with以及全局作用域。

var A = function(){
  var user = {}
};
A();

我們在執行這個函數的時候就會創建一個函數作用域,也會創建一個局部的變量user。這個user只能在函數作用域裏面使用,函數執行完畢之後作用域銷燬,同時對象也失去引用,其引用的對象下次的垃圾回收時就會釋放。這裏的user是一個小對象,就會分配在新生代的from空間中。

我們知道作用域會有一個作用域鏈的概念,就是在當前的作用域找不到變量之後就會向父級的作用域尋找,一直向外擴散,找不到就會拋出未定義的錯誤。

小結:

  • 閉包沒有及時釋放會造成內存泄漏,常見的有定時器
  • 全局變量,此種變量需要進程退出纔會被釋放,引用的對象就會在常駐內存中(老生代

V8堆內存相關的指標

我們可以調用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的堆內存。

04 小結

Node將JavaScript的主要應用擴展到了服務器端,所以我們考慮的細節也要和瀏覽器的不同,更多的是對內存資源的分配問題,稍有不慎可能會寫出一些內存泄漏的問題,使得垃圾回收機制不能清理釋放內存,嚴重可能會導致服務器崩潰。

所以平時我們寫代碼也要謹慎一點,不要寫全局變量或者頻繁使用閉包,這些如果沒有正確釋放內存的話也會導致內存泄漏。內存泄漏的本質其實就是應當回收的對象但是卻沒有被回收(轉移到老生代空間)。

參考文章:

  • 深入淺出Node.js
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章