您好,登錄后才能下訂單哦!
這篇文章主要介紹“Node怎么最小化堆分配和防止內存泄漏”,在日常操作中,相信很多人在Node怎么最小化堆分配和防止內存泄漏問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Node怎么最小化堆分配和防止內存泄漏”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
內存管理問題在計算機領域中一直備受關注。在計算機中運行的每個軟件,都會被分配到計算機有限內存的一小部分。這些內存必須得認真管理,在合適的時間進行分配或者釋放。
Nodejs
可以通過其高效的自動垃圾回收機制,來處理內存管理的繁瑣任務,從而將開發人員解放出來,從事其他任務。雖然說 Nodejs
已經幫助開發者解決了內存管理的問題,但是在面對大型應用開發的過程中,對于開發者理解 V8
和 Nodejs
中的內存管理機制仍然非常重要。
這片文章主要介紹了如何在堆中分配和釋放內存,并且幫助你知道如何最小化堆分配和防止內存泄漏。
Nodejs
中的堆分配JavaScript
和 Node.js
為你抽象了很多東西,并且在后臺完成了大部分繁重的工作。
我們知道,當一段代碼被執行的時候,代碼中的變量和對象會被存儲在棧內存或者堆內存中,JavaScript
代碼會被存儲在將要被執行的執行上下文中。
ECMAScript
規范本身并沒有規定如何分配和管理內存。這是一個依賴于 JavaScript
引擎和底層系統架構的實現細節。深入理解引擎是如何處理變量的已經超出了本文的范圍,但如果你想了解更多關于V8
是如何做到這一點的,請參考文章JavaScript內存模型揭秘
和數據是如何存儲在V8 JS引擎內存中的?
。
Node.js
中高效的堆內存使用很重要?存儲在堆中的內存變量將一直存在,除非它被垃圾收集器刪除或釋放。堆內存是一大塊連續的內存塊,即使再被分配和釋放之后,仍然會保持這種狀態。
不幸的是,由于堆內存收集和釋放方式,內存可能會被浪費,從而導致泄漏。
V8
使用的是分代垃圾收集機制,即它將對象劃分為不同的代(新生代和老生代)。代空間又會被劃分為不同的區——例如新生代由新空間組成,老生代會被劃分為舊空間、映射空間和大對象空間。新對象最初被分配到新生代空間中,當新生代空間使用完時,垃圾收集器將執行清理機制以釋放空間。在一次 GC
運行中幸存下來的對象會被復制到新生代的中間中間中,在第二輪運行中幸存下來的對象會被移動到老生代中。
由于運行程序先進行內存收集,占用了寶貴的虛擬內存資源,因此當不再需要內存時,程序必須釋放內存,這就是內存釋放。
此外,如果內存被釋放了(不管先前它在堆中的哪個位置釋放),堆內存將被合并為一個連續的內存塊形式。由于堆內存復雜性的增加,在這里存儲會導致更高的性能開銷(但使得后續的存儲有了更大的靈活性)。
雖然 Nodejs
擁有高效的垃圾回收機制,但是堆內存的低效使用可能導致內存泄漏。應用程序可能會占用太多的內存,甚至崩潰。
垃圾回收器會尋找并釋放孤立的內存空間,但有時它可能無法跟蹤每一塊內存。這可能導致不必要的負載增加,特別是對于大型應用程序。稍后我們將詳細討論 Nodejs
中的垃圾收集器是如何工作的。
導致內存泄漏的一些最常見的原因包括:
多重引用
全局變量
閉包
計時器
事件
使用多個變量指針保持對一個對象的引用是非常常見的操作。雖然這對你來說非常方便,但如果對對象的其中一個引用被垃圾回收器收集,而其他引用沒有被收集,則也可能導致內存泄漏。
在 Node.js
和 JavaScript
應用程序中,被忘記清理的計時器和回調函數也是導致內存泄漏的兩個常見原因。被綁定到計時器的對象直到超時才會被垃圾收集。如果計時器一直運行,則被引用的對象將永遠不會被垃圾回收器收集。即使沒有變量指針引用對象,也會發生這種情況,因此將在堆中造成內存泄漏。
思考下示例代碼:
const language = () => {
console.log("Javascript");】
// 遞歸自身
setTimeout(() => language(), 1000);
}
上面這段代碼將會被一直運行,并且永遠不會被垃圾回收器回收
Nodejs
中的內存泄漏這有幾個工具可以用于檢測和調試 Nodejs
中的內存泄漏,包括 Chrome DevTools
,Node
的進程。memoryUsage API
和 AppSignal
的垃圾收集器看板。
Chrome DevTools
可能是最簡單的工具之一。要啟動調試器,需要以 inspect
模式啟動 Node
。運行node --inspect
來執行此操作。
更具體地說,如果你的 Node
的入口是 app.js
,你需要運行 node --inspect app.js
來調試Node 應用程序。然后,打開 Chromium
瀏覽器,進入 chrome://inspect
。你也可以在 Edge://inspect 打開檢查器頁面。在檢查器頁面,你應該看到這樣一個頁面:
注意,你正在嘗試調試的 Node
應用程序出現在檢查器頁面的底部。單擊 inspect
打開調試器。調試器有兩個重要的選項卡—— Memory
和 Profiler
——但在本討論中,我們將重點關注 Memory
選項卡。
使用 Chrome
調試器查找內存泄漏最簡單的方法是使用堆快照
。快照可以幫助你檢查一些變量或檢查它們的保留區大小。
你也可以通過比較多張快照發現內存泄漏。對于一個實力來說,你可以在內存泄漏之前和之后分別保存一張快照,然后比較兩者。為了獲取快照,你可以通過在 Heap snapshot
上點擊一下,然后點擊 *Take snapshot
按鈕。這可能需要一些時間,這取決于應用程序的 Total JS
堆大小。你也可以通過點擊 DevTool
底部的 load
按鈕來加載現有的快照。
當你有了兩張或者多張快照時,你就可以非常容易的比較堆分配,已找到內存泄漏的原因。你可以通過以下方式查看快照:
Summary
:根據構造函數名稱對 Node
應用程序中的對象進行分組展示
Comparison
: 顯示兩張快照之間的區別
Containment
:允許你查看堆內并分析全局名稱空間中引用的對象
Statistics
:
在 DevTools
堆分析器中有兩列很突出——即 Shallow Size
和 Retained Size
。
Shallow Size
表示的是對象自身在內存中的大小。這個內存大小對于大多數對象來說并不大,但數組和字符串類型除外。另一方面, Retained Size
是黨有問題的對象和依賴對象被釋放或從根節點無法訪問時釋放的內存大小。
Chrome DevTools
并不是獲取堆快照的唯一方法。如果你使用的是 nodejs
12.0 或更高版本,你還可以通過運行 node --heapsnapshot-signal
命令:
node --heapsnapshot-signal=SIGUSR2 app.js
雖然可以使用任何標志,但建議使用用戶定義的信號SIGUSR1
或SIGUSR2
。
如果你從正在服務端運行的應用中獲取一張對快照,則可以使用 V8
包中的 writeHeapSnapshot
函數:
require("v8").writeHeapSnapshot();
這個方法要求 Nodejs
的版本高于 11.13。在早期的版本中,你可以使用相關的包來實現。
使用 Chrome DevTools
獲取堆快照并不是調試內存問題的唯一方法。你也可以使用Allocation instrumentation on timeline
跟蹤每個堆分配的情況。
內存分配時間軸顯示了隨時間變化的測量內存分配的情況。要啟用此功能,需要先啟動分析器(Profiler
),然后運行應用程序示例以開始調試內存問題。如果你希望記錄長時間運行的內存分配操作,并想要更小的性能開銷,那么最好的選擇是分配抽樣方法。
Node
的 process.memoryUsage
API你也可以使用 Node
的 process.memoryUsage
API來觀察內存使用情況。運行 process.memoryUsage
,你可以訪問以下內容:
rss
:已分配的內存量
heapTotal
:已分配堆的總大小
heapUsed
:當執行進程時被使用內存總量
arrayBuffers
:為 Buffer 實例分配的內存大小
AppSignal
的垃圾收集器看板為了可視化堆的變化情況,AppSignal
提供了一個方便的垃圾收集看板。當你將 Node.js
應用連接到AppSignal
時,這個看板會自動為你生成!
看看這個例子,在“V8 Heap Statistics
”圖表中,你可以清楚地看到內存使用的峰值:
如果看板中中的數據出現一個穩定增長的趨勢,這意味著你的代碼中或者依賴中存在內存泄漏的情況。
了解更多關于 Node.js 的AppSignal。
如果你知道如何發現內存泄漏,但如何修復它們?我們可能很快就知道。但是首先重要的是理解 Nodejs
和 V8
是如何進行垃圾收集的。
垃圾回收機制會在不需要的時候釋放內存。為了更高效的工作,垃圾回收算法必須正確的定義和識別不需要再內存中繼續存儲的內容。
在引用計數 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
(標記清除算法)回收算法,非常有用。但是,你必須小心并顯式地使一個對象從根節點不可訪問,以確保它被垃圾收集。
這有一些方法可以提高內存使用率并避免內存泄漏。
全局變量包括使用 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
,那么你可以在 V8
和Chrome
中將性能提高 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
清除計時器是至關重要的。
當你不再需要使用 setTimeout
或 setimmediation
創建計時器時,調用 clearTimeout
或clearImmediate
也是一個很好的實踐。
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
,但 Person1
和 Person2
都被保存在作用域中。
這會消耗更多內存,并造成內存泄漏。為此,在面臨上面這種情況時,你最好僅聲明你需要的,將不需要的重置為 null
。
例如:
const func = () => {
let Person1 = { name: "Samuel", age: 25, language: "English" };
let Person2 = { name: "Den", age: 23, language: "Dutch" };
Person1 = null;
return () => Person2;
};
具有較長生命周期的觀察器和事件發射器可能是內存泄漏的來源,特別是如果你在不再需要它們時沒有取消訂閱的話。
代碼示例:
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怎么最小化堆分配和防止內存泄漏”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。