日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

作者:elvinpeng,騰訊 WXG 前端開發工程師

Node.js 使用的是 V8 引擎,會自動進行垃圾回收(Garbage Collection,GC),因而寫代碼的時候不需要像 C/C++ 一樣手動分配、釋放內存空間,方便不少,不過仍然需要注意內存的使用,避免造成內存泄漏(Memory Leak)。

內存泄漏往往非常隱蔽,例如下面這段代碼你能看出來是哪兒里有問題嗎?

let theThing = null;
let replaceThing = function() {
  const newThing = theThing;
  const unused = function() {
    if (newThing) console.log("hi");
  };
  // 不斷修改引用
  theThing = {
    longStr: new Array(1e8).join("*"),
    someMethod: function() {
      console.log("a");
    },
  };

  // 每次輸出的值會越來越大
  console.log(process.memoryUsage().heapUsed);
};

setInterval(replaceThing, 100);

如果可以的話,歡迎加入我們微信支付境外團隊,一起不斷追求卓越。如果暫時看不出來的話,一起來讀讀這篇文章吧。

文章的前半部分會先介紹一些理論知識,然后再舉一個定位內存泄漏的例子,感興趣的朋友可以直接先看看 這個例子。

整體結構

全局變量、事件綁定、緩存爆炸?Node.js內存泄漏問題分析

 

從上圖中,可以看到 Node.js 的常駐內存(Resident Set)分為堆和棧兩個部分,具體為:

    • 指針空間(Old pointer space):存儲的對象含有指向其它對象的指針。
    • 數據空間(Old data space):存儲的對象僅含有數據(不含指向其它對象的指針),例如從新生代移動過來的字符串等。
    • 新生代(New Space/Young Generation):用來臨時存儲新對象,空間被等分為兩份,整體較小,采用 Scavenge(Minor GC) 算法進行垃圾回收。
    • 老生代(Old Space/Old Generation):用來存儲存活時間超過兩個 Minor GC 時間的對象,采用 標記清除 & 整理(Mark-Sweep & Mark-Compact,Major GC) 算法進行垃圾回收,內部可再劃分為兩個空間:
    • 代碼空間(Code Space):用于存放代碼段,是唯一的可執行內存(不過過大的代碼段也有可能存放在大對象空間)。
    • 大對象空間(Large Object Space):用于存放超過其它空間對象限制(Page::kMaxRegularHeapObjectSize)的大對象(可以參考這個 V8 Commit),存放在此的對象不會在垃圾回收的時候被移動。
    • ...
  • 棧:用于存放原始的數據類型,函數調用時的入棧出棧也記錄于此。

棧的空間由操作系統負責管理,開發者無需過于關心;堆的空間由 V8 引擎進行管理,可能由于代碼問題出現內存泄漏,或者長時間運行后,垃圾回收導致程序運行速度變慢。

我們可以通過下面代碼簡單的觀察 Node.js 內存使用情況:

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

const memoryUsage = process.memoryUsage();

console.log(JSON.stringify({
    rss: format(memoryUsage.rss), // 常駐內存
    heapTotal: format(memoryUsage.heapTotal), // 總的堆空間
    heapUsed: format(memoryUsage.heapUsed), // 已使用的堆空間
    external: format(memoryUsage.external), // C++ 對象相關的空間
}, null, 2));

external 是 C++ 對象相關的空間,例如通過 new ArrayBuffer(100000); 申請一塊 Buffer 內存的時候,可以明顯看到 external 空間的增加。

可以通過下列參數調整相關空間的默認大小,單位為 MB:

  • --stack_size 調整棧空間
  • --min_semi_space_size 調整新生代半空間的初始值
  • --max_semi_space_size 調整新生代半空間的最大值
  • --max-new-space-size 調整新生代空間的最大值
  • --initial_old_space_size 調整老生代空間的初始值
  • --max-old-space-size 調整老生代空間的最大值

其中比較常用的是 --max_new_space_size 和 --max-old-space-size。

新生代的 Scavenge 回收算法、老生代的 Mark-Sweep & Mark-Compact 算法相關的文章已經很多,這里就不贅述了,例如這篇文章講的不錯 Node.js 內存管理和 V8 垃圾回收機制。

內存泄漏

由于不當的代碼,有時候難免會發生內存泄漏,常見的有四個場景:

  1. 全局變量
  2. 閉包引用
  3. 事件綁定
  4. 緩存爆炸

接下來分別舉個例子講一講。

全局變量

沒有使用 var/let/const 聲明的變量會直接綁定在 Global 對象上(Node.js 中)或者 windows 對象上(瀏覽器中),哪怕不再使用,仍不會被自動回收:

function test() {
  x = new Array(100000);
}

test();
console.log(x);

這段代碼的輸出為 [ <100000 empty items> ],可以看到 test 函數運行完后,數組 x 仍未被釋放。

閉包引用

閉包引發的內存泄漏往往非常隱蔽,例如下面這段代碼你能看出來是哪兒里有問題嗎?

let theThing = null;
let replaceThing = function() {
  const newThing = theThing;
  const unused = function() {
    if (newThing) console.log("hi");
  };
  // 不斷修改引用
  theThing = {
    longStr: new Array(1e8).join("*"),
    someMethod: function() {
      console.log("a");
    },
  };

  // 每次輸出的值會越來越大
  console.log(process.memoryUsage().heapUsed);
};

setInterval(replaceThing, 100);

運行這段代碼可以看到輸出的已使用堆內存越來越大,而其中的關鍵就是因為 在目前的 V8 實現當中,閉包對象是當前作用域中的所有內部函數作用域共享的,也就是說 theThing.someMethod 和 unUsed 共享同一個閉包的 context,導致 theThing.someMethod 隱式的持有了對之前的 newThing 的引用,所以會形成 theThing -> someMethod -> newThing -> 上一次 theThing ->... 的循環引用,從而導致每一次執行 replaceThing 這個函數的時候,都會執行一次 longStr: new Array(1e8).join("*"),而且其不會被自動回收,導致占用的內存越來越大,最終內存泄漏。

對于上面這個問題有一個很巧妙的解決方法:通過引入新的塊級作用域,將 newThing 的聲明、使用與外部隔離開,從而打破共享,阻止循環引用。

let theThing = null;
let replaceThing = function() {
  {
    const newThing = theThing;
    const unused = function() {
      if (newThing) console.log("hi");
    };
  }
  // 不斷修改引用
  theThing = {
    longStr: new Array(1e8).join("*"),
    someMethod: function() {
      console.log("a");
    },
  };

  console.log(process.memoryUsage().heapUsed);
};

setInterval(replaceThing, 100);

這里通過 { ... } 形成了單獨的塊級作用域,而且在外部沒有引用,從而 newThing 在 GC 的時候會被自動回收,例如在我的電腦運行這段代碼輸出如下:

2097128
2450104
2454240
...
2661080
2665200
2086736 // 此時進行垃圾回收釋放了內存
2093240

事件綁定

事件綁定導致的內存泄漏在瀏覽器中非常常見,一般是由于事件響應函數未及時移除,導致重復綁定或者 DOM 元素已移除后未處理事件響應函數造成的,例如下面這段 React 代碼:

class Test extends React.Component {
  componentDidMount() {
    window.addEventListener('resize', function() {
      // 相關操作
    });
  }

  render() {
    return <div>test component</div>;
  }
}

<Test /> 組件在掛載的時候監聽了 resize 事件,但是在組件移除的時候沒有處理相應函數,假如 <Test /> 的掛載和移除非常頻繁,那么就會在 window 上綁定很多無用的事件監聽函數,最終導致內存泄漏。可以通過如下的方式避免這個問題:

class Test extends React.Component {
  componentDidMount() {
    window.addEventListener('resize', this.handleResize);
  }

  handleResize() { ... }

  componentWillUnmount() {
    window.removeEventListener('resize', this.handleResize);
  }

  render() {
    return <div>test component</div>;
  }
}

緩存爆炸

通過 Object/Map 的內存緩存可以極大地提升程序性能,但是很有可能未控制好緩存的大小和過期時間,導致失效的數據仍緩存在內存中,導致內存泄漏:

const cache = {};

function setCache() {
  cache[Date.now()] = new Array(1000);
}

setInterval(setCache, 100);

上面這段代碼中,會不斷的設置緩存,但是沒有釋放緩存的代碼,導致內存最終被撐爆。

如果的確需要進行內存緩存的話,強烈建議使用 lru-cache 這個 npm 包,可以設置緩存有效期和最大的緩存空間,通過 LRU 淘汰算法來避免緩存爆炸。

內存泄漏定位實操

當出現內存泄漏的時候,定位起來往往十分麻煩,主要有兩個原因:

  1. 程序開始運行的時候,問題不會立即暴露,需要持續的運行一段時間,甚至一兩天,才會復現問題。
  2. 出錯的提示信息非常模糊,往往只能看到 heap out of memory 錯誤信息。

在這種情況下,可以借助兩個工具來定問題:Chrome DevTools 和 heapdump。heapdump的作用就如同它的名字所說 - 將內存中堆的狀態信息生成快照(snapshot)導出,然后我們將其導入到 Chrome DevTools 中看到具體的詳情,例如堆中有哪些對象、占據多少空間等等。

接下來通過上文中閉包引用里內存泄漏的例子,來實際操作一把。首先 npm install heapdump 安裝后,修改代碼為下面的樣子:

// 一段存在內存泄漏問題的示例代碼
const heapdump = require('heapdump');

heapdump.writeSnapshot('init.heapsnapshot'); // 記錄初始內存的堆快照

let i = 0; // 記錄調用次數
let theThing = null;
let replaceThing = function() {
  const newThing = theThing;
  let unused = function() {
    if (newThing) console.log("hi");
  };

  // 不斷修改引用
  theThing = {
    longStr: new Array(1e8).join("*"),
    someMethod: function() {
      console.log("a");
    },
  };

  if (++i >= 1000) {
    heapdump.writeSnapshot('leak.heapsnapshot'); // 記錄運行一段時間后內存的堆快照
    process.exit(0);
  }
};

setInterval(replaceThing, 100);

在第 3 行和第 22 行,分別導出了初始狀態的快照和循環了 1000 次后的快照,保存為 init.heapsnapshot 與 leak.heapsnapshot。

然后打開 Chrome 瀏覽器,按下 F12 調出 DevTools 面板,點擊 Memory 的 Tab,最后通過 Load 按鈕將剛剛的兩個快照依次導入:

全局變量、事件綁定、緩存爆炸?Node.js內存泄漏問題分析

mark

導入后,在左側可以看到堆內存有明顯的上漲,從 1.7 MB 上漲到了 3.1 MB,幾乎翻了一倍:

全局變量、事件綁定、緩存爆炸?Node.js內存泄漏問題分析

 

接下來就是最關鍵的步驟了,點擊 leak 快照,然后將其與 init 快照進行對比:

全局變量、事件綁定、緩存爆炸?Node.js內存泄漏問題分析

 

右側紅框圈出來了兩列:

  • Delta:表示變化的數量
  • Size Delta:表述變化的空間大小

可以看到增長最大的前兩項是 拼接的字符串(concatenated string ) 和 閉包(closure),那么我們點開來看看具體有哪些:

全局變量、事件綁定、緩存爆炸?Node.js內存泄漏問題分析

 


全局變量、事件綁定、緩存爆炸?Node.js內存泄漏問題分析

 

從這兩個圖中,可以很直觀的看出來主要是 theThing.someMethod 這個函數的閉包上下文和 theThing.longStr 這個很長的拼接字符串造成的內存泄漏,到這里問題就基本定位清楚了,我們還可以點擊下方的 Object 模塊來更清楚的看一下調用鏈的關系:

全局變量、事件綁定、緩存爆炸?Node.js內存泄漏問題分析

 

圖中很明顯的看出來,內存泄漏原因就是因為 newTHing <- 閉包上下文 <- someMethod<- 上一次 newThing 這樣的鏈式依賴關系導致內存的快速增長。圖中第二列的 distance 表示的是該變量距離根節點的距離,因而最上級的 newThing 是最遠的,表示的是下級引用上級的關系。

參考文章

  1. Visualizing memory management in V8 Engine
  2. Github - 內存泄漏的例子
  3. ali node - 正確打開 Chrome devtools

分享到:
標簽:Node js
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定