首頁 >web前端 >js教程 >Node學習如何最小化堆分配和防止記憶體洩漏

Node學習如何最小化堆分配和防止記憶體洩漏

青灯夜游
青灯夜游轉載
2023-01-11 20:25:392263瀏覽

Node.js如何查看記憶體洩漏?以下這篇文章帶大家了解Nodejs堆分配,介紹如何最小化堆分配和防止記憶體洩漏,希望對大家有幫助!

Node學習如何最小化堆分配和防止記憶體洩漏

記憶體管理問題在電腦領域中一直備受關注。在電腦中運行的每個軟體,都會被分配到電腦有限記憶體的一小部分。這些記憶體必須得認真管理,在適當的時間進行分配或釋放。

Nodejs 可以透過其高效的自動垃圾回收機制,來處理記憶體管理的繁瑣任務,從而將開發人員解放出來,從事其他任務。雖然說Nodejs 已經幫助開發者解決了記憶體管理的問題,但在面對大型應用程式開發的過程中,對於開發者理解V8Nodejs中的記憶體管理機制仍然非常重要。

這片文章主要介紹如何在堆中分配和釋放內存,並且幫助你知道如何最小化堆分配和防止內存洩漏。 【相關教學推薦:nodejs影片教學程式設計教學

Nodejs 中的堆疊分配

JavaScriptNode.js 為你抽象化了許多東西,並且在後台完成了大部分繁重的工作。

我們知道,當一段程式碼被執行的時候,程式碼中的變數和物件會被儲存在堆疊記憶體或堆疊記憶體中,JavaScript 程式碼會被儲存在將要執行的執行上下文中。

ECMAScript 規格本身並沒有規定如何分配和管理記憶體。這是一個依賴 JavaScript 引擎和底層系統架構的實作細節。深入理解引擎是如何處理變數的已經超出了本文的範圍,但如果你想了解更多關於V8是如何做到這一點的,請參考文章JavaScript記憶體模型揭秘資料是如何儲存在V8 JS引擎記憶體中的?

為什麼在Node.js 中高效的堆記憶體使用很重要?

##儲存在堆中的記憶體變數會一直存在,除非它被垃圾收集器刪除或釋放。堆記憶體是一大塊連續的記憶體區塊,即使再被分配和釋放之後,仍然會保持這種狀態。

不幸的是,由於堆記憶體收集和釋放方式,記憶體可能會被浪費,導致洩漏。

V8 使用的是分代垃圾收集機制,即它將物件劃分為不同的代(新生代和老生代)。代空間又會被劃分為不同的區域-例如新生代由新空間組成,老生代會被劃分為舊空間、映射空間和大物件空間。新物件最初被分配到新生代空間中,當新生代空間使用完時,垃圾收集器將執行清理機制以釋放空間。在一次 GC 運行中倖存下來的物件會被複製到新生代的中間中間中,在第二輪運行中倖存下來的物件會被移動到老生代中。

由於運行程式先進行記憶體收集,佔用了寶貴的虛擬記憶體資源,因此當不再需要記憶體時,程式必須釋放內存,這就是記憶體釋放。

此外,如果記憶體被釋放了(不管先前它在堆中的哪個位置釋放),堆記憶體將被合併為一個連續的記憶體區塊形式。由於堆記憶體複雜性的增加,在這裡儲存會導致更高的效能開銷(但使得後續的儲存有了更大的靈活性)。

雖然

Nodejs 擁有高效率的垃圾回收機制,但是堆疊記憶體的低效率使用可能導致記憶體洩漏。應用程式可能會佔用太多的內存,甚至崩潰。

Nodejs 堆記憶體洩漏的原因

垃圾回收器會尋找並釋放孤立的記憶體空間,但有時它可能無法追蹤每一塊記憶體。這可能導致不必要的負載增加,特別是對於大型應用程式。稍後我們將詳細討論

Nodejs 中的垃圾收集器是如何運作的。

導致記憶體洩漏的一些最常見的原因包括:

  • 多重引用
  • 全域變數
  • 閉包
  • 計時器
  • 事件

使用多個變數指標保持對一個物件的參考是非常常見的操作。雖然這對你來說非常方便,但如果對物件的其中一個引用被垃圾回收器收集,而其他引用沒有被收集,則也可能導致記憶體洩漏。

Node.jsJavaScript 應用程式中,被忘記清理的計時器和回呼函數也是導致記憶體洩漏的兩個常見原因。被綁定到計時器的物件直到逾時才會被垃圾收集。如果計時器一直運行,則被引用的物件將永遠不會被垃圾回收器收集。即使沒有變數指標引用對象,也會發生這種情況,因此將在堆中造成記憶體洩漏。

思考下範例程式碼:

const language = () => {
  console.log("Javascript");】
  // 递归自身
  setTimeout(() => language(), 1000);
}

上面這段程式碼將會被一直運行,並且永遠不會被垃圾回收器回收

##如何發現Nodejs 中的記憶體洩漏

這有幾個工具可以用來偵測和偵錯

Nodejs 中的記憶體洩漏,包括Chrome DevToolsNode 的進程。 memoryUsage APIAppSignal 的垃圾收集器看板。

使用 Chrome DevTools

Chrome DevTools可能是最簡單的工具之一。要啟動偵錯器,需要以 inspect 模式啟動 Node。執行node --inspect來執行此操作。

更具體地說,如果你的

Node 的入口是app.js,你需要執行node --inspect app.js來調試Node 應用程式。然後,開啟 Chromium 瀏覽器,進入 chrome://inspect。你也可以在 Edge://inspect 開啟檢查器頁面。在檢查器頁面,你應該看到這樣一個頁面:

注意,你正在嘗試偵錯的

Node 應用程式出現在檢查器頁面的底部。點選 inspect 開啟偵錯器。偵錯器有兩個重要的選項卡— MemoryProfiler —但在本討論中,我們將重點放在 Memory 選項卡。

使用

Chrome 偵錯器尋找記憶體洩漏最簡單的方法是使用堆快照。快照可以幫助你檢查一些變數或檢查它們的保留區大小。

你也可以透過比較多張快照發現記憶體洩漏。對於一個實力來說,你可以在記憶體洩漏之前和之後分別保存一張快照,然後比較兩者。為了取得快照,你可以透過在

Heap snapshot 上點擊一下,然後點擊 *Take snapshot 按鈕。這可能需要一些時間,這取決於應用程式的 Total JS 堆大小。你也可以點選 DevTool 底部的 load 按鈕來載入現有的快照。

當你有了兩張或多張快照時,你就可以非常容易的比較堆分配,已找到記憶體洩漏的原因。你可以透過以下方式查看快照:

  • Summary:根據建構函式名稱對Node 應用程式中的物件進行分組展示

  • Comparison#:顯示兩張快照之間的差異

  • Containment:允許你查看堆內並分析全域名稱空間中引用的物件

  • Statistics

DevTools 堆疊分析器中有兩列很突出-即Shallow SizeRetained Size

Shallow Size 表示的是物件本身在記憶體中的大小。這個記憶體大小對於大多數物件來說並不大,但數組和字串類型除外。另一方面, Retained Size 是黨有問題的物件和依賴物件被釋放或從根節點無法存取時釋放的記憶體大小。

Chrome DevTools 並不是取得堆疊快照的唯一方法。如果你使用的是nodejs 12.0 或更高版本,你也可以透過執行 node --heapsnapshot-signal 指令:

node --heapsnapshot-signal=SIGUSR2 app.js

雖然可以使用任何標誌,但建議使用使用者定義的訊號

SIGUSR1SIGUSR2

如果你從正在服務端運行的應用程式中取得一張對快照,則可以使用

V8 套件中的 writeHeapSnapshot 函數:

require("v8").writeHeapSnapshot();

这个方法要求 Nodejs 的版本高于 11.13。在早期的版本中,你可以使用相关的包来实现。

使用 Chrome DevTools 获取堆快照并不是调试内存问题的唯一方法。你也可以使用Allocation instrumentation on timeline 跟踪每个堆分配的情况。

内存分配时间轴显示了随时间变化的测量内存分配的情况。要启用此功能,需要先启动分析器(Profiler),然后运行应用程序示例以开始调试内存问题。如果你希望记录长时间运行的内存分配操作,并想要更小的性能开销,那么最好的选择是分配抽样方法。

通过 Nodeprocess.memoryUsage API

你也可以使用 Nodeprocess.memoryUsage API来观察内存使用情况。运行 process.memoryUsage,你可以访问以下内容:

  • rss:已分配的内存量
  • heapTotal:已分配堆的总大小
  • heapUsed:当执行进程时被使用内存总量
  • arrayBuffers:为 Buffer 实例分配的内存大小

使用 AppSignal 的垃圾收集器看板

为了可视化堆的变化情况,AppSignal 提供了一个方便的垃圾收集看板。当你将 Node.js 应用连接到AppSignal 时,这个看板会自动为你生成!

看看这个例子,在“V8 Heap Statistics”图表中,你可以清楚地看到内存使用的峰值:

如果看板中中的数据出现一个稳定增长的趋势,这意味着你的代码中或者依赖中存在内存泄漏的情况。

了解更多关于 Node.js 的AppSignal。

垃圾回收机制工作原理

如果你知道如何发现内存泄漏,但如何修复它们?我们可能很快就知道。但是首先重要的是理解 NodejsV8 是如何进行垃圾收集的。

垃圾回收机制会在不需要的时候释放内存。为了更高效的工作,垃圾回收算法必须正确的定义和识别不需要再内存中继续存储的内容。

在引用计数 GC 算法中,如果堆中的对象在堆栈中不再有引用,则该对象将被垃圾收集。该算法通过计数引用来工作——因此,如果引用计数为零,则对象将进行垃圾收集。尽管这个算法大多数时候都有效,但它在处理循环引用的情况时却失效了。

看一下代码示例:

let data = {};
data.el = data; 
let obj1 = {};
let obj2 = {};
obj1.a = obj2;
obj2.a = obj1;

具有循环引用的对象永远不会被清除作用域或被垃圾回收器回收,即使不再需要或使用它们。这会形成内存泄漏,并使应用程序效率低下。值得庆幸的是,Node.js 不再使用这种算法进行垃圾回收。

JavaScript 中的最上层对象是一个全局对象。在浏览器中,是 window 对象,但在 Nodejs 中,是 global 对象。该算法比引用计数算法更高效,并解决了循环引用的问题。

考虑到上面的例子,虽然 obj1 和 obj2 仍然存在循环引用,但如果它们不再从顶级对象可访问(不再需要),它们将被垃圾收集。

这种算法,通常称为 mark and sweep (标记清除算法)回收算法,非常有用。但是,你必须小心并显式地使一个对象从根节点不可访问,以确保它被垃圾收集。

修复 Nodejs App 中的内存泄漏

这有一些方法可以提高内存使用率并避免内存泄漏。

避免全局变量

全局变量包括使用 var 关键字声明的变量、this 关键字声明的变量和未使用关键字声明的变量。

我们已经偶然声明的全局变量(以及任何其他形式的全局变量)会导致内存泄漏。它们总是可以从全局对象访问,因此除非显式地设置为 null,否则不能被垃圾收集。

考虑下面的例子:

function variables() {
  this.a = "Variable one";  
  var b = "Variable two";
  c = "Variable three";
}

这三个变量都是全局变量。为了避免使用全局变量,可以考虑在文件顶部添加 use strict 指令来切换strict 模式。

使用 JSON.parse

JSON 的语法比 JavaScript 简单得多,因此它比 JavaScript 对象更容易解析。

事实上,如果你使用一个大型 JavaScript 对象,通过将其转化为字符串形式,使用时解析为 JSON,那么你可以在 V8Chrome 中将性能提高 1.7 倍。

在其他 JavaScript 引擎(如Safari)中,性能可能会更好。在 Webpack 中使用这种优化方法来提高前端应用程序的性能。

例如,不使用以下 JavaScript 对象:

const Person = { name: "Samuel", age: 25, language: "English" };

更有效的方法是将它们进行字符串化,然后将其解析为JSON

const Person = JSON.parse('{"name":"Samuel","age":25,"language":"English"}');

将大数据处理拆分为块并创建子进程

你获取在实际业务中会当处理大型数据时,遇到一些奇观的内存溢出的问题,例如大的 CSV 文件。当然,你可以通过扩展你的应用内存上限去处理任务,但是最好的方法是通过将大块数据分割为多个小块(chunks)。

在一些情况下,在多核机器上扩展 Node.js 应用程序可能会有所帮助。这涉及到将应用程序分离为主进程和工作进程。worker 处理繁重的逻辑,而 master 控制 worker 并在内存耗尽时重新启动它们。

有效使用计时器

我们创建的计时器可能会造成内存泄漏。为了提高堆内存管理,确保你的计时器不会永远运行。

特别是,使用 setInterval 创建计时器时,当不再需要计时器时调用 clearInterval 清除计时器是至关重要的。

当你不再需要使用 setTimeoutsetimmediation 创建计时器时,调用 clearTimeoutclearImmediate 也是一个很好的实践。

const timeout = setTimeout(() => {
  console.log("timeout");
}, 1500);
 
const immediate = setImmediate(() => {
  console.log("immediate");
});
 
const interval = setInterval(() => {
  console.log("interval");
}, 500);
 
clearTimeout(timeout);
clearImmediate(immediate);
clearInterval(interval);

移除闭包中不在需要的变量

JavaScript 中,闭包是一个常见概念。例如存在函数嵌套或者回调函数。如果在函数中使用了一个变量,当函数返回时,它将被标记为垃圾收集,但闭包可不是这样的。

代码示例:

const func = () => {
  let Person1 = { name: "Samuel", age: 25, language: "English" };
  let Person2 = { name: "Den", age: 23, language: "Dutch" };
 
  return () => Person2;
};

上面函数会一直引用父级作用域并将每个变量保存在作用域中。换句话说,虽然你仅仅使用了 Person2,但 Person1Person2 都被保存在作用域中。

这会消耗更多内存,并造成内存泄漏。为此,在面临上面这种情况时,你最好仅声明你需要的,将不需要的重置为 null

例如:

const func = () => {
  let Person1 = { name: "Samuel", age: 25, language: "English" };
  let Person2 = { name: "Den", age: 23, language: "Dutch" };
  Person1 = null;
  return () => Person2;
};

取消订阅观察者和 Event Emitters

具有较长生命周期的观察器和事件发射器可能是内存泄漏的来源,特别是如果你在不再需要它们时没有取消订阅的话。

代码示例:

const EventEmitter = require("events").EventEmitter;
const emitter = new EventEmitter();
 
const bigObject = {}; //Some big object
const listener = () => {
  doSomethingWith(bigObject);
};
emitter.on("event1", listener);

在这里,我们保留 bigObject 的内存,直到侦听器从发射器中释放,或者发射器被垃圾收集。为了解决这个问题,我们需要调用 removeEventListener 从发射器中释放监听器。

emitter.removeEventListener("event1", listener);

当连接到发射器的事件侦听器超过 10 个时,也可能发生内存泄漏。大多数情况下,你可以通过编写更高效的代码来解决这个问题。

但是,在某些情况下,你可能需要显式地设置最大事件侦听器。

例如:

emitter.setMaxListeners(n);

总结

在这篇文章中,我们探索了如何最小化你的堆和检测 Node.js 中的内存泄漏。

我们首先研究了 Node 中的堆分配,包括堆栈和堆的工作方式。然后,我们考虑了跟踪内存使用情况和内存泄漏的原因的重要性。

接下来,我们看到了如何使用 Chrome DevTools , Node 的进程来查找内存泄漏。memoryUsage API和 AppSignal 的垃圾收集可视化看板。

最后,我们发现了垃圾收集是如何工作的,并分享了一些修复应用程序内存泄漏的方法。

像其他程式語言一樣,記憶體管理在 JavaScriptNode.js 中非常重要。我希望這篇介紹對你有用。編碼快樂!

原文連結: Minimize Heap Allocations in Node.js

##更多node相關知識,請訪問:

nodejs 教程

以上是Node學習如何最小化堆分配和防止記憶體洩漏的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除