理解和調優記憶體

Node.js 基於谷歌的 V8 JavaScript 引擎構建,為在伺服器端執行 JavaScript 提供了強大的執行時。然而,隨著應用程式的增長,管理記憶體成為維持最佳效能和處理記憶體洩漏或崩潰等問題的關鍵任務。在本文中,我們將探討如何在 Node.js 中監控、管理和最佳化記憶體使用。我們還將涵蓋重要的 V8 概念,如堆和垃圾回收,並討論如何使用命令列標誌來微調記憶體行為。

V8 如何管理記憶體

V8 的核心將記憶體分為幾個部分,其中兩個主要區域是。理解這些空間,特別是堆是如何管理的,是改善應用程式記憶體使用的關鍵。

V8 的記憶體管理基於分代假設,即大多數物件生命週期很短。因此,它將堆分為幾代以最佳化垃圾回收。

  1. 新生代:這是分配新的、短生命週期物件的地方。這裡的物件被期望“早夭”,因此垃圾回收會頻繁發生,從而快速回收記憶體。

    例如,假設你有一個每秒接收 1,000 個請求的 API。每個請求都會生成一個臨時物件,如 { name: 'John', age: 30 },該物件在請求處理完畢後即被丟棄。如果你將新生代空間的大小保持預設值,V8 會頻繁執行次要垃圾回收來清理這些小物件,確保記憶體使用保持在可控範圍內。

  2. 老生代:在新生代中經過多次垃圾回收週期後仍然存活的物件會被提升到老生代。這些通常是長生命週期的物件,如使用者會話、快取資料或持久化狀態。因為這些物件傾向於存活更長時間,所以該空間的垃圾回收發生頻率較低,但更消耗資源。

    假設你正在執行一個跟蹤使用者會話的應用程式。每個會話可能會儲存像 { userId: 'abc123', timestamp: '2025-04-10T12:00:00', sessionData: {...} } 這樣的資料,只要使用者處於活動狀態,這些資料就需要保留在記憶體中。隨著併發使用者數量的增長,老生代空間可能會被填滿,導致記憶體不足錯誤或由於垃圾回收效率低下而響應變慢。

在 V8 中,JavaScript 物件、陣列和函式的記憶體都在中分配。堆的大小不是固定的,超過可用記憶體會導致“記憶體不足”錯誤,使你的應用程式崩潰。

要檢查當前的堆大小限制,你可以使用 v8 模組。

const  = ('node:v8');
const {  } = .();
const  =  / (1024 * 1024 * 1024);

.(`${} GB`);

這將以 GB 為單位輸出最大堆大小,該大小基於你係統的可用記憶體。

除了堆之外,V8 還使用進行記憶體管理。棧是一塊用於儲存區域性變數和函式呼叫資訊的記憶體區域。與由 V8 的垃圾回收器管理的堆不同,棧遵循後進先出 (LIFO) 的原則。

每當呼叫一個函式時,一個新的幀就會被推入棧中。當函式返回時,它的幀會被彈出。與堆相比,棧的大小要小得多,但記憶體分配和釋放速度更快。然而,棧的大小有限,過度使用記憶體(例如深度遞迴)可能導致棧溢位

監控記憶體使用情況

在調整記憶體使用之前,瞭解你的應用程式正在消耗多少記憶體非常重要。Node.js 和 V8 提供了多種監控記憶體使用的工具。

使用 process.memoryUsage()

process.memoryUsage() 方法提供了關於你的 Node.js 程序正在使用多少記憶體的見解。它返回一個包含詳細資訊的物件,例如:

  • rss (Resident Set Size,常駐集大小):分配給你程序的總記憶體,包括堆和其他區域。
  • heapTotal:為堆分配的總記憶體。
  • heapUsed:堆中當前正在使用的記憶體。
  • external:由外部資源(如與 C++ 庫的繫結)使用的記憶體。
  • arrayBuffers:分配給各種類 Buffer 物件的記憶體。

以下是如何使用 process.memoryUsage() 來監控應用程式中的記憶體使用情況:

console.log(process.memoryUsage());

輸出將顯示每個區域正在使用的記憶體量:

{
  "rss": 25837568,
  "heapTotal": 5238784,
  "heapUsed": 3666120,
  "external": 1274076,
  "arrayBuffers": 10515
}

透過隨時間監控這些值,你可以識別記憶體使用是否在意外增加。例如,如果 heapUsed 持續增長而沒有被釋放,這可能表明你的應用程式存在記憶體洩漏。

用於記憶體調優的命令列標誌

Node.js 提供了幾個命令列標誌來微調與記憶體相關的設定,從而讓你最佳化應用程式的記憶體使用。

--max-old-space-size

此標誌為 V8 堆中儲存長生命週期物件的老生代大小設定了限制。如果你的應用程式使用大量記憶體,你可能需要調整此限制。

例如,假設你的應用程式處理持續不斷的傳入請求,每個請求都會生成一個大物件。隨著時間的推移,如果這些物件沒有被清理,老生代空間可能會過載,導致崩潰或響應變慢。

你可以透過設定 --max-old-space-size 標誌來增加老生代的大小:

node --max-old-space-size=4096 app.js

這將老生代的大小設定為 4096 MB (4 GB),如果你的應用程式正在處理大量持久化資料(如快取或使用者會話資訊),這將特別有用。

--max-semi-space-size

此標誌控制 V8 堆中新生代的大小。新生代是新建立物件被分配並頻繁進行垃圾回收的地方。增加此大小可以減少次要垃圾回收週期的頻率。

例如,如果你有一個接收大量請求的 API,每個請求都會建立像 { name: 'Alice', action: 'login' } 這樣的小物件,你可能會因為頻繁的垃圾回收而注意到效能下降。透過增加新生代的大小,你可以減少這些回收的頻率並提高整體效能。

node --max-semi-space-size=64 app.js

這將新生代的大小增加到 64 MB,允許更多物件在觸發垃圾回收前駐留在記憶體中。這在物件建立和銷燬頻繁的高吞吐量環境中特別有用。

--gc-interval

此標誌調整垃圾回收週期的發生頻率。預設情況下,V8 會確定最佳間隔,但在某些需要更多控制記憶體清理的場景下,你可以覆蓋此設定。

例如,在一個像股票交易平臺這樣的即時應用程式中,你可能希望透過減少回收頻率來最小化垃圾回收的影響,確保應用程式可以處理資料而不會出現明顯停頓。

node --gc-interval=100 app.js

此設定強制 V8 每 100 毫秒嘗試進行一次垃圾回收。你可能需要針對特定用例調整此間隔,但要謹慎:設定過低的間隔可能因過多的垃圾回收週期而導致效能下降。

--expose-gc

使用 --expose-gc 標誌,你可以從應用程式程式碼內部手動觸發垃圾回收。這在特定場景下很有幫助,比如在處理完一批大資料後,你希望在繼續進行後續操作前回收記憶體。

要暴露 gc,請使用以下命令啟動你的應用:

node --expose-gc app.js

然後,在你的應用程式程式碼中,你可以呼叫 global.gc() 來手動觸發垃圾回收:

global.gc();

請記住,手動觸發垃圾回收並不會停用正常的 GC 演算法。V8 仍然會根據需要執行自動垃圾回收。手動呼叫是補充性的,應謹慎使用,因為過度使用會對效能產生負面影響。

其他資源

要深入瞭解 V8 如何處理記憶體,請檢視 V8 團隊的這些帖子:

融會貫通

透過調整老生代和新生代的大小設定、有選擇地觸發垃圾回收以及配置堆限制,你可以最佳化應用程式的記憶體使用並提高其整體效能。這些工具使你能夠在高需求場景下更好地管理記憶體,並在應用程式擴充套件時保持穩定性。