溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Node.js中出現(xiàn)內(nèi)存泄漏的原因有哪些

發(fā)布時(shí)間:2021-07-21 09:28:46 來(lái)源:億速云 閱讀:143 作者:Leah 欄目:編程語(yǔ)言

本篇文章給大家分享的是有關(guān)Node.js中出現(xiàn)內(nèi)存泄漏的原因有哪些,小編覺(jué)得挺實(shí)用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說(shuō),跟著小編一起來(lái)看看吧。

內(nèi)存泄漏(Memory  Leak)指由于疏忽或錯(cuò)誤造成程序未能釋放已經(jīng)不再使用的內(nèi)存的情況。如果內(nèi)存泄漏的位置比較關(guān)鍵,那么隨著處理的進(jìn)行可能持有越來(lái)越多的無(wú)用內(nèi)存,這些無(wú)用的內(nèi)存變多會(huì)引起服務(wù)器響應(yīng)速度變慢,嚴(yán)重的情況下導(dǎo)致內(nèi)存達(dá)到某個(gè)極限(可能是進(jìn)程的上限,如  v8 的上限;也可能是系統(tǒng)可提供的內(nèi)存上限)會(huì)使得應(yīng)用程序崩潰。

傳統(tǒng)的 C/C++ 中存在野指針,對(duì)象用完之后未釋放等情況導(dǎo)致的內(nèi)存泄漏。而在使用虛擬機(jī)執(zhí)行的語(yǔ)言中如 Java、JavaScript 由于使用了 GC (Garbage Collection,垃圾回收)機(jī)制自動(dòng)釋放內(nèi)存,使得程序員的精力得到的極大的解放,不用再像傳統(tǒng)語(yǔ)言那樣時(shí)刻對(duì)于內(nèi)存的釋放而戰(zhàn)戰(zhàn)兢兢。

但是,即便有了 GC 機(jī)制可以自動(dòng)釋放,但這并不意味這內(nèi)存泄漏的問(wèn)題不存在了。內(nèi)存泄漏依舊是開(kāi)發(fā)者們不能繞過(guò)的一個(gè)問(wèn)題,今天讓我們來(lái)了解如何分析 Node.js 中的內(nèi)存泄漏。

GC in Node.js

Node.js 使用 V8 作為 JavaScript 的執(zhí)行引擎,所以討論 Node.js 的 GC 情況就等于在討論 V8 的 GC。在 V8 中一個(gè)對(duì)象的內(nèi)存是否被釋放,是看程序中是否還有地方持有改對(duì)象的引用。

在 V8 中,每次 GC 時(shí),是根據(jù) root 對(duì)象 (瀏覽器環(huán)境下的 window,Node.js 環(huán)境下的 global )  依次梳理對(duì)象的引用,如果能從 root 的引用鏈到達(dá)訪問(wèn),V8  就會(huì)將其標(biāo)記為可到達(dá)對(duì)象,反之為不可到達(dá)對(duì)象。被標(biāo)記為不可到達(dá)對(duì)象(即無(wú)引用的對(duì)象)后就會(huì)被 V8 回收。更多細(xì)節(jié),可以參見(jiàn) alinode  的 解讀 V8 GC。

了解上述的點(diǎn)之后,你就會(huì)知道,在 Node.js 中內(nèi)存泄露的原因就是本該被清除的對(duì)象,被可到達(dá)對(duì)象引用以后,未被正確的清除而常駐內(nèi)存。

內(nèi)存泄漏的幾種情況

一、全局變量

a = 10;//未聲明對(duì)象。global.b = 11;//全局變量引用

這種比較簡(jiǎn)單的原因,全局變量直接掛在 root 對(duì)象上,不會(huì)被清除掉。

二、閉包

function out() {  const bigData = new Buffer(100);
  inner = function () {void bigData;
  }
}

閉包會(huì)引用到父級(jí)函數(shù)中的變量,如果閉包未釋放,就會(huì)導(dǎo)致內(nèi)存泄漏。上面例子是 inner 直接掛在了 root 上,那么每次執(zhí)行 out 函數(shù)所產(chǎn)生的 bigData 都不會(huì)釋放,從而導(dǎo)致內(nèi)存泄漏。

需要注意的是,這里舉得例子只是簡(jiǎn)單的將引用掛在全局對(duì)象上,實(shí)際的業(yè)務(wù)情況可能是掛在某個(gè)可以從 root 追溯到的對(duì)象上導(dǎo)致的。

三、事件監(jiān)聽(tīng)

Node.js 的事件監(jiān)聽(tīng)也可能出現(xiàn)的內(nèi)存泄漏。例如對(duì)同一個(gè)事件重復(fù)監(jiān)聽(tīng),忘記移除(removeListener),將造成內(nèi)存泄漏。這種情況很容易在復(fù)用對(duì)象上添加事件時(shí)出現(xiàn),所以事件重復(fù)監(jiān)聽(tīng)可能收到如下警告:

(node:2752) Warning: Possible EventEmitter memory leak  detected。11 haha listeners added。Use emitter。setMaxListeners() to  increase limit

例如,Node.js 中 Agent 的 keepAlive 為 true 時(shí),可能造成的內(nèi)存泄漏。當(dāng) Agent keepAlive 為  true 的時(shí)候,將會(huì)復(fù)用之前使用過(guò)的 socket,如果在 socket 上添加事件監(jiān)聽(tīng),忘記清除的話,因?yàn)?socket  的復(fù)用,將導(dǎo)致事件重復(fù)監(jiān)聽(tīng)從而產(chǎn)生內(nèi)存泄漏。

原理上與前一個(gè)添加事件監(jiān)聽(tīng)的時(shí)候忘了清除是一樣的。在使用 Node.js 的 http 模塊時(shí),不通過(guò) keepAlive 復(fù)用是沒(méi)有問(wèn)題的,復(fù)用了以后就會(huì)可能產(chǎn)生內(nèi)存泄漏。所以,你需要了解添加事件監(jiān)聽(tīng)的對(duì)象的生命周期,并注意自行移除。

關(guān)于這個(gè)問(wèn)題的實(shí)例,可以看 Github 上的 issues(node Agent keepAlive 內(nèi)存泄漏)

四、其他原因

還有一些其他的情況可能會(huì)導(dǎo)致內(nèi)存泄漏,比如緩存。在使用緩存的時(shí)候,得清楚緩存的對(duì)象的多少,如果緩存對(duì)象非常多,得做限制***緩存數(shù)量處理。還有就是非常占用  CPU 的代碼也會(huì)導(dǎo)致內(nèi)存泄漏,服務(wù)器在運(yùn)行的時(shí)候,如果有高 CPU 的同步代碼,因?yàn)镹ode.js  是單線程的,所以不能處理處理請(qǐng)求,請(qǐng)求堆積導(dǎo)致內(nèi)存占用過(guò)高。

定位內(nèi)存泄漏

一、重現(xiàn)內(nèi)存泄漏情況

想要定位內(nèi)存泄漏,通常會(huì)有兩種情況:

  1. 對(duì)于只要正常使用就可以重現(xiàn)的內(nèi)存泄漏,這是很簡(jiǎn)單的情況只要在測(cè)試環(huán)境模擬就可以排查了。

  2. 對(duì)于偶然的內(nèi)存泄漏,一般會(huì)與特殊的輸入有關(guān)系。想穩(wěn)定重現(xiàn)這種輸入是很耗時(shí)的過(guò)程。如果不能通過(guò)代碼的日志定位到這個(gè)特殊的輸入,那么推薦去生產(chǎn)環(huán)境打印內(nèi)存快照了。需要注意的是,打印內(nèi)存快照是很耗 CPU 的操作,可能會(huì)對(duì)線上業(yè)務(wù)造成影響。

快照工具推薦使用 heapdump 用來(lái)保存內(nèi)存快照,使用 devtool 來(lái)查看內(nèi)存快照。使用 heapdump 保存內(nèi)存快照時(shí),只會(huì)有  Node.js 環(huán)境中的對(duì)象,不會(huì)受到干擾(如果使用 node-inspector 的話,快照中會(huì)有前端的變量干擾)。

PS:安裝 heapdump 在某些 Node.js 版本上可能出錯(cuò),建議使用 npm install heapdump -target=Node.js 版本來(lái)安裝。

二、打印內(nèi)存快照

將 heapdump 引入代碼中,使用 heapdump.writeSnapshot 就可以打印內(nèi)存快照了了。為了減少正常變量的干擾,可以在打印內(nèi)存快照之前會(huì)調(diào)用主動(dòng)釋放內(nèi)存的 gc() 函數(shù)(啟動(dòng)時(shí)加上 –expose-gc 參數(shù)即可開(kāi)啟)。

const heapdump = require('heapdump');const save = function () {
  gc();
  heapdump.writeSnapshot('./' + Date.now() + '.heapsnapshot');
}

在打印線上的代碼的時(shí)候,建議按照內(nèi)存增長(zhǎng)情況來(lái)打印快照。heapdump 可以使用 kill 向程序發(fā)送信號(hào)來(lái)打印內(nèi)存快照(只在 *nix 系統(tǒng)上提供)。

kill -USR2 <pid>

推薦打印 3 個(gè)內(nèi)存快照,一個(gè)是內(nèi)存泄漏之前的內(nèi)存快照,一個(gè)是少量測(cè)試以后的內(nèi)存快照,還有一個(gè)是多次測(cè)試以后的內(nèi)存快照。

***個(gè)內(nèi)存快照作為對(duì)比,來(lái)查看在測(cè)試后有哪些對(duì)象增長(zhǎng)。在內(nèi)存泄漏不明顯的情況下,可以與大量測(cè)試以后的內(nèi)存快照對(duì)比,這樣能更容易定位。

三、對(duì)比內(nèi)存快照找出泄漏位置

通過(guò)內(nèi)存快照找到數(shù)量不斷增加的對(duì)象,找到增加對(duì)象是被誰(shuí)給引用,找到問(wèn)題代碼,改正之后就行,具體問(wèn)題具體分析,這里通過(guò)我們?cè)诠ぷ髦杏龅降那闆r來(lái)講解。

const {EventEmitter} = require('events');const heapdump = require('heapdump');

global.test = new EventEmitter();
heapdump.writeSnapshot('./' + Date.now() + '.heapsnapshot');function run3() {  const innerData = new Buffer(100);  const outClosure3 = function () {void innerData;
  };
  test.on('error', () => {console.log('error');
  });
  outClosure3();
}for(let i = 0; i < 10; i++) {
  run3();
}
gc();

heapdump.writeSnapshot('./' + Date.now() + '.heapsnapshot');

這里是對(duì)錯(cuò)誤代碼的最小重現(xiàn)代碼。

首先使用 node &ndash;expose-gc index.js 運(yùn)行代碼,將會(huì)得到兩個(gè)內(nèi)存快照,之后打開(kāi) devtool,點(diǎn)擊 profile,載入內(nèi)存快照。打開(kāi)對(duì)比,Delta 會(huì)顯示對(duì)象的變化情況,如果對(duì)象 Delta 一直增長(zhǎng),就很有可能是內(nèi)存泄漏了。

Node.js中出現(xiàn)內(nèi)存泄漏的原因有哪些

可以看到有三處對(duì)象明顯增長(zhǎng)的地方,閉包、上下文以及 Buffer 對(duì)象增長(zhǎng)。點(diǎn)擊查看一下對(duì)象的引用情況:

Node.js中出現(xiàn)內(nèi)存泄漏的原因有哪些

其實(shí)這三處對(duì)象增長(zhǎng)都是一個(gè)問(wèn)題導(dǎo)致的。test 對(duì)象中的 error 監(jiān)聽(tīng)事件中閉包引用了 innerData 對(duì)象,導(dǎo)致 buffer 沒(méi)有被清除,從而導(dǎo)致內(nèi)存泄漏。

其實(shí)這里的 error 監(jiān)聽(tīng)事件中沒(méi)有引用 innerData 為什么會(huì)閉包引用了 innerData 對(duì)象,這個(gè)問(wèn)題很是疑惑,后來(lái)弄清是 V8 的優(yōu)化問(wèn)題,在文末會(huì)額外講解一下。對(duì)于對(duì)比快照找到問(wèn)題,得看你對(duì)代碼的熟悉程度,還有眼力了。

如何避免內(nèi)存泄漏

文中的例子基本都可以很清楚的看出內(nèi)存泄漏,但是在工作中,代碼混合上業(yè)務(wù)以后就不一定能很清楚的看出內(nèi)存泄漏了,還是得依靠工具來(lái)定位內(nèi)存泄漏。另外下面是一些避免內(nèi)存泄漏的方法。

  1. ESLint 檢測(cè)代碼檢查非期望的全局變量。

  2. 使用閉包的時(shí)候,得知道閉包了什么對(duì)象,還有引用閉包的對(duì)象何時(shí)清除閉包。***可以避免寫(xiě)出復(fù)雜的閉包,因?yàn)閺?fù)雜的閉包引起的內(nèi)存泄漏,如果沒(méi)有打印內(nèi)存快照的話,是很難看出來(lái)的。

  3. 綁定事件的時(shí)候,一定得在恰當(dāng)?shù)臅r(shí)候清除事件。在編寫(xiě)一個(gè)類的時(shí)候,推薦使用 init 函數(shù)對(duì)類的事件監(jiān)聽(tīng)進(jìn)行綁定和資源申請(qǐng),然后 destroy 函數(shù)對(duì)事件和占用資源進(jìn)行釋放。

額外說(shuō)明

在做了很多測(cè)試以后得到下面關(guān)于閉包的總結(jié)。

class Test{};
global.test = new Test()function run5(bigData) {  const innerData = new Buffer(100);  // 被閉包引用,創(chuàng)建一個(gè) context: context1。
  // context1 引用 bigData,innerData。
  // closure 為 function run5()
  // run5函數(shù)沒(méi)有 context,所以 context1 沒(méi)有previous。
  // 在 run5中新建的函數(shù)將綁定上 context1。

  test.outClosure5 = function () {// 此函數(shù)閉包 context 指向 context1。void bigData;const closureData = new Buffer(100);// 被閉包使用,創(chuàng)建 context: context2。// outClosure5 函數(shù)有 context1,previous 指向 context1。// 在 outClosure5 中新建的函數(shù)將綁定上context2。test.innerClosure5 = function () {      // 此函數(shù)閉包 context 指向 context2。  void innerData;
    }
    test.innerClosure5_1 = function () {      // 此函數(shù)閉包 context 指向 context2。  void closureData;
    }
  };
  test.outClosure5_1 = function () {

  }
  test.outClosure5();
}

run5(new Buffer(1000));

以上就是Node.js中出現(xiàn)內(nèi)存泄漏的原因有哪些,小編相信有部分知識(shí)點(diǎn)可能是我們?nèi)粘9ぷ鲿?huì)見(jiàn)到或用到的。希望你能通過(guò)這篇文章學(xué)到更多知識(shí)。更多詳情敬請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI