內存泄漏每每很是隱蔽,例以下面這段代碼你能看出來是哪兒裏有問題嗎?node
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);
複製代碼
若是能夠的話,歡迎加入咱們微信支付境外團隊 😊git
若是暫時看不出來的話,一塊兒來讀讀這篇文章吧。github
文章的前半部分會先介紹一些理論知識,而後再舉一個定位內存泄漏的例子,感興趣的朋友能夠直接先看看 這個例子。算法
從上圖中,能夠看到 Node.js 的常駐內存(Resident Set)分爲堆和棧兩個部分,具體爲:npm
Scavenge(Minor GC)
算法進行垃圾回收。標記清除 & 整理(Mark-Sweep & Mark-Compact,Major GC)
算法進行垃圾回收,內部可再劃分爲兩個空間:
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
。markdown
新生代的 Scavenge 回收算法、老生代的 Mark-Sweep & Mark-Compact 算法相關的文章已經不少,這裏就不贅述了,例如這篇文章講的不錯 Node.js內存管理和V8垃圾回收機制。
因爲不當的代碼,有時候不免會發生內存泄漏,常見的有四個場景:
接下來分別舉個例子講一講。
沒有使用 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 淘汰算法來避免緩存爆炸。
當出現內存泄漏的時候,定位起來每每十分麻煩,主要有兩個緣由:
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
按鈕將剛剛的兩個快照依次導入:
導入後,在左側能夠看到堆內存有明顯的上漲,從 1.7 MB 上漲到了 3.1 MB,幾乎翻了一倍:
接下來就是最關鍵的步驟了,點擊 leak
快照,而後將其與 init
快照進行對比:
右側紅框圈出來了兩列:
Delta
:表示變化的數量Size Delta
:表述變化的空間大小能夠看到增加最大的前兩項是 拼接的字符串(concatenated string )
和 閉包(closure)
,那麼咱們點開來看看具體有哪些:
從這兩個圖中,能夠很直觀的看出來主要是 theThing.someMethod
這個函數的閉包上下文和 theThing.longStr
這個很長的拼接字符串形成的內存泄漏,到這裏問題就基本定位清楚了,咱們還能夠點擊下方的 Object
模塊來更清楚的看一下調用鏈的關係:
圖中很明顯的看出來,內存泄漏緣由就是由於 newTHing
-> 閉包上下文 -> someMethod
-> 上一次 newThing
這樣的鏈式不斷調用致使內存的快速增加。
若是對我寫的內容感興趣的話,歡迎關注個人筆記本 爛筆頭 ,會隨手記錄一些有意思的東西。