聊聊Node.js中的 GC (垃圾回收)機制

2022-11-29 22:00:42
是如何做 GC (垃圾回收)的?下面本篇文章就來帶大家瞭解一下。

node.js極速入門課程:進入學習

GC,Garbage Collection,垃圾回收。在程式設計中,一般指的是記憶體自動回收機制,會定時將不需要用到的資料進行清除。

Node.js 底層使用了 V8 引擎。V8 是 Google 開源的一款高效能 JavaScript 引擎,使用了 C++ 進行編寫。【相關教學推薦:】

Node.js 的記憶體主要分成三部分:

  • 程式碼空間:存放程式碼段的地方;

  • 棧:函數呼叫棧產生的臨時變數,為一些基本型別,比如數位、字串、布林值,以及物件參照(儲存的是地址,不儲存物件本身)。

  • 堆:存放物件等資料;

堆記憶體

Node.js 底層使用的是 V8,下面講解一下 V8 的記憶體回收機制。

首先 JS 中所有的物件都會儲存在堆記憶體中。在建立程序的時候,會分配一個初始大小的堆記憶體,然後我們的物件就會放到裡面。

當物件越來越多,堆記憶體會不夠用,此時堆記憶體會動態地擴大。如果到達一個最大限制(現在通常是 4GB),就會堆記憶體溢位的錯誤,然後終止 Node.js 程序。

新生代與老生代

V8 首先將記憶體分成兩部分,或者說兩個生代(generation):

  • 新生代(yong generation):儲存一些存活時間較短的物件;

  • 老生代(old generation):儲存存活時間長或者長駐的物件。

新生代很小,這裡會存放一些存活時間很短的物件,通常它們會被頻繁地回收(比如函數的呼叫棧的一些臨時物件)。

新生代可通過 node --max-semi-space-size=SIZE index.js 修改新生代的大小,單位為 MB。

另外,老生代則通過 --max-old-space-size=SIZE 來設定

新生代的 Scavenge 演演算法

新生代使用了 Scavenge 演演算法,是一種基於 copy(複製)的演演算法。

新生代會分成兩個空間,這種空間稱為 semispace,它們為:

  • From 空間:新宣告的物件會放入這裡

  • To 空間:用作搬移的空間

新宣告的物件會放入到 From 空間中,From 空間的物件緊密排布,通過指標,上一物件緊貼下一個物件,是記憶體連續的,不用擔心記憶體碎片的問題。

所謂記憶體碎片,指的是空間分配不均勻,產生大量小的連續空間,無法放入一個大物件。

當 From 空間快滿了,我們就會遍歷找出活躍物件,將它們 copy 到 To 空間。此時 From 空間其實就空了,然後我們將 From 和 To 互換身份。

如果一些物件被 copy 了多次,會被認為存活時間較長,將被移動到老生代中。

這種基於 copy 的演演算法,優點是可以很好地處理記憶體碎片的問題,缺點是會浪費一些空間作為搬移的空間位置,此外因為拷貝比較耗費時間,所以不適合分配太大的記憶體空間,更多是做一種輔助 GC。

Mark-Sweep 和 Mark-Compact

老生代的空間就比新生代要大得多了,放的是一些存活時間長的物件,用的是 Mark-Sweep (標記清除)演演算法。

首先是標記階段。從根集 Root Set(執行棧和全域性物件)往上找到所有能存取到的物件,給它們標記為活躍物件。

標記完後,就是清除階段,將沒有標記的物件清除,其實就是標記一下這個記憶體地址為空閒。

這種做法會導致 空閒記憶體空間碎片化,當我們建立了一個大的連續物件,就會找不到地方放下。這時候,就要用 Mark-Compact(標記整理)來將碎片的活躍物件做一個整合。

Mark-Compact 會將所有活躍物件拷貝移動到一端,然後邊界的另一邊就是一整塊的連續可用記憶體了。

考慮到 Mark-Sweep 和 Mark-Compact 花費的時間很長,且會阻塞 JavaScript 的執行緒,所以通常我們不會一次性做完,而是用 增量標記 (Incremental Marking)的方式。也就是做斷斷續續地標記,小步走,垃圾回收和應用邏輯交替進行。

另外,V8 還做了並行標記和並行清理,提高執行效率。

圖片

檢視記憶體相關資訊

我們可以通過 process.memoryUsage 方法拿到記憶體相關的一些資訊。

process.memoryUsage();
登入後複製

輸出內容為:

{
  rss: 35454976,
  heapTotal: 7127040,
  heapUsed: 5287088,
  external: 958852,
  arrayBuffers: 11314
}
登入後複製

說明

  • rss:常駐記憶體大小(resident set size),包括程式碼片段、堆記憶體、棧等部分。

  • heapTotal:V8 的堆記憶體總大小;

  • heapUsed:佔用的堆記憶體;

  • external:V8 之外的的記憶體大小,指的是 C++ 物件佔用的記憶體,比如 Buffer 資料。

  • arrayBuffers:ArrayBufferSharedArrayBuffer 相關的記憶體大小,屬於 external 的一部分。

以上數位的單位都是位元組。

測試最大記憶體限制

寫一個指令碼,用一個定時器,讓一個陣列不停地變大,並列印堆記憶體使用情況,直到記憶體溢位。

const format = function (bytes) {
  return (bytes / 1024 / 1024).toFixed(2) + " MB";
};

const printMemoryUsage = function () {
  const memoryUsage = process.memoryUsage();
  console.log(
    `heapTotal: ${format(memoryUsage.heapTotal)}, heapUsed: ${format(
      memoryUsage.heapUsed
    )}`
  );
};

const bigArray = [];
setInterval(function () {
  bigArray.push(new Array(20 * 1024 * 1024));
  printMemoryUsage();
}, 500);
登入後複製

需要特別注意的是,不要用 Buffer 做測試。

因為 Buffer 是 Node.js 特有的處理二進位制的物件,它不是在 V8 中的實現的,是 Node.js 用 C++ 另外實現的,不通過 V8 分配記憶體,屬於堆外記憶體。

我使用電腦是 macbook pro M1 Pro,Node.js 版本為 v16.17.0,使用的 V8 版本是 9.4.146.26-node.22(通過 process.versions.v8 得到)。

輸出結果為(省略了一些多餘的資訊):

heapTotal: 164.81 MB, heapUsed: 163.93 MB
heapTotal: 325.83 MB, heapUsed: 323.79 MB
heapTotal: 488.59 MB, heapUsed: 483.84 MB
...
heapTotal: 4036.44 MB, heapUsed: 4003.37 MB
heapTotal: 4196.45 MB, heapUsed: 4163.29 MB

<--- Last few GCs --->

[28033:0x140008000]    17968 ms: Mark-sweep 4003.2 (4036.4) -> 4003.1 (4036.4) MB, 2233.8 / 0.0 ms  (average mu = 0.565, current mu = 0.310) allocation failure scavenge might not succeed
[28033:0x140008000]    19815 ms: Mark-sweep 4163.3 (4196.5) -> 4163.1 (4196.5) MB, 1780.3 / 0.0 ms  (average mu = 0.413, current mu = 0.036) allocation failure scavenge might not succeed


<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
...
登入後複製

可以看到,是在 4000 MB 之後超出了記憶體上限,發生堆溢位,然後退出了程序。說明在我的機器上,預設的最大記憶體為 4G。

實際最大記憶體和它執行所在的機器有關,如果你的機器的記憶體大小為 2G,最大記憶體將設定為 1.5G。

更多node相關知識,請存取:!

以上就是聊聊Node.js中的 GC (垃圾回收)機制的詳細內容,更多請關注TW511.COM其它相關文章!