JavaScript 內存泄露教程

BroderickCo 7年前發布 | 16K 次閱讀 內存泄露 JavaScript開發 JavaScript

一、什么是內存泄露?

程序的運行需要內存。只要程序提出要求,操作系統或者運行時(runtime)就必須供給內存。

對于持續運行的服務進程(daemon),必須及時釋放不再用到的內存。否則,內存占用越來越高,輕則影響系統性能,重則導致進程崩潰。

不再用到的內存,沒有及時釋放,就叫做內存泄露(memory leak)。

有些語言(比如 C 語言)必須手動釋放內存,程序員負責內存管理。

char  buffer;
buffer = (char) malloc(42);

// Do something with buffer

free(buffer);</code></pre>

上面是 C 語言代碼, malloc 方法用來申請內存,使用完畢之后,必須自己用 free 方法釋放內存。

這很麻煩,所以大多數語言提供自動內存管理,減輕程序員的負擔,這被稱為"垃圾回收機制"(garbage collector)。

二、垃圾回收機制

垃圾回收機制怎么知道,哪些內存不再需要呢?

最常使用的方法叫做 "引用計數" (reference counting):語言引擎有一張"引用表",保存了內存里面所有的資源(通常是各種值)的引用次數。如果一個值的引用次數是 0 ,就表示這個值不再用到了,因此可以將這塊內存釋放。

上圖中,左下角的兩個值,沒有任何引用,所以可以釋放。

如果一個值不再需要了,引用數卻不為 0 ,垃圾回收機制無法釋放這塊內存,從而導致內存泄露。

const arr = [1, 2, 3, 4];
console.log('hello world');

上面代碼中,數組 [1, 2, 3, 4] 是一個值,會占用內存。變量 arr 是僅有的對這個值的引用,因此引用次數為 1 。盡管后面的代碼沒有用到 arr ,它還是會持續占用內存。

如果增加一行代碼,解除 arr 對 [1, 2, 3, 4] 引用,這塊內存就可以被垃圾回收機制釋放了。

const arr = [1, 2, 3, 4];
console.log('hello world');
arr = null;

上面代碼中, arr 重置為 null ,就解除了對 [1, 2, 3, 4] 的引用,引用次數變成了 0 ,內存就可以釋放出來了。

因此,并不是說有了垃圾回收機制,程序員就輕松了。你還是需要關注內存占用:那些很占空間的值,一旦不再用到,你必須檢查是否還存在對它們的引用。如果是的話,就必須手動解除引用。

三、內存泄露的識別方法

怎樣可以觀察到內存泄露呢?

經驗法則 是,如果連續五次垃圾回收之后,內存占用一次比一次大,就有內存泄露。這就要求實時查看內存占用。

3.1 瀏覽器

Chrome 瀏覽器查看內存占用,按照以下步驟操作。

  1. 打開開發者工具,選擇 Timeline 面板
  2. 在頂部的 Capture 字段里面勾選 Memory
  3. 點擊左上角的錄制按鈕。
  4. 在頁面上進行各種操作,模擬用戶的使用情況。
  5. 一段時間后,點擊對話框的 stop 按鈕,面板上就會顯示這段時間的內存占用情況。

如果內存占用基本平穩,接近水平,就說明不存在內存泄露。

反之,就是內存泄露了。

3.2 命令行

命令行可以使用 Node 提供的 process.memoryUsage 方法。

console.log(process.memoryUsage());
// { rss: 27709440,
//  heapTotal: 5685248,
//  heapUsed: 3449392,
//  external: 8772 }

process.memoryUsage 返回一個對象,包含了 Node 進程的內存占用信息。該對象包含四個字段,單位是字節, 含義 如下。

  • rss(resident set size):所有內存占用,包括指令區和堆棧。
  • heapTotal:"堆"占用的內存,包括用到的和沒用到的。
  • heapUsed:用到的堆的部分。
  • external: V8 引擎內部的 C++ 對象占用的內存。

判斷內存泄露,以 heapUsed 字段為準。

四、WeakMap

前面說過,及時清除引用非常重要。但是,你不可能記得那么多,有時候一疏忽就忘了,所以才有那么多內存泄露。

最好能有一種方法,在新建引用的時候就聲明,哪些引用必須手動清除,哪些引用可以忽略不計,當其他引用消失以后,垃圾回收機制就可以釋放內存。這樣就能大大減輕程序員的負擔,你只要清除主要引用就可以了。

ES6 考慮到了這一點,推出了兩種新的數據結構:WeakSet 和WeakMap。它們對于值的引用都是不計入垃圾回收機制的,所以名字里面才會有一個"Weak",表示這是弱引用。

下面以 WeakMap 為例,看看它是怎么解決內存泄露的。

const wm = new WeakMap();

const element = document.getElementById('example');

wm.set(element, 'some information'); wm.get(element) // "some information"</code></pre>

上面代碼中,先新建一個 Weakmap 實例。然后,將一個 DOM 節點作為鍵名存入該實例,并將一些附加信息作為鍵值,一起存放在 WeakMap 里面。這時,WeakMap 里面對 element 的引用就是弱引用,不會被計入垃圾回收機制。

也就是說,DOM 節點對象的引用計數是 1 ,而不是 2 。這時,一旦消除對該節點的引用,它占用的內存就會被垃圾回收機制釋放。Weakmap 保存的這個鍵值對,也會自動消失。

基本上,如果你要往對象上添加數據,又不想干擾垃圾回收機制,就可以使用 WeakMap。

五、WeakMap 示例

WeakMap 的例子很難演示,因為無法觀察它里面的引用會自動消失。此時,其他引用都解除了,已經沒有引用指向 WeakMap 的鍵名了,導致無法證實那個鍵名是不是存在。

我一直想不出辦法,直到有一天賀師俊老師 提示 ,如果引用所指向的值占用特別多的內存,就可以通過 process.memoryUsage 方法看出來。

根據這個思路,網友 vtxf 補充了下面的 例子 。

首先,打開 Node 命令行。

$ node --expose-gc

上面代碼中, --expose-gc 參數表示允許手動執行垃圾回收機制。

然后,執行下面的代碼。

// 手動執行一次垃圾回收,保證獲取的內存使用狀態準確
> global.gc(); 
undefined

// 查看內存占用的初始狀態,heapUsed 為 4M 左右 > process.memoryUsage(); { rss: 21106688, heapTotal: 7376896, heapUsed: 4153936, external: 9059 }

> const wm = new WeakMap(); undefined

> const b = new Object(); undefined

> global.gc(); undefined

// 此時,heapUsed 仍然為 4M 左右 > process.memoryUsage(); { rss: 20537344, heapTotal: 9474048, heapUsed: 3967272, external: 8993 }

// 在 WeakMap 中添加一個鍵值對, // 鍵名為對象 b,鍵值為一個 510241024 的數組
> wm.set(b, new Array(510241024)); WeakMap {}

// 手動執行一次垃圾回收 > global.gc(); undefined

// 此時,heapUsed 為 45M 左右 > process.memoryUsage(); { rss: 62652416, heapTotal: 51437568, heapUsed: 45911664, external: 8951 }

// 解除對象 b 的引用
> b = null; null

// 再次執行垃圾回收 > global.gc(); undefined

// 解除 b 的引用以后,heapUsed 變回 4M 左右 // 說明 WeakMap 中的那個長度為 510241024 的數組被銷毀了 > process.memoryUsage(); { rss: 20639744, heapTotal: 8425472, heapUsed: 3979792, external: 8956 }</code></pre>

上面代碼中,只要外部的引用消失,WeakMap 內部的引用,就會自動被垃圾回收清除。由此可見,有了它的幫助,解決內存泄露就會簡單很多。

六、參考鏈接

 

 

來自:http://www.ruanyifeng.com/blog/2017/04/memory-leak.html

 

 本文由用戶 BroderickCo 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!