1. 程式人生 > >JavaScript學習總結(二十三)——JavaScript 內存泄漏教程

JavaScript學習總結(二十三)——JavaScript 內存泄漏教程

blog reference example 什麽 負責 size 對象 let 令行

參考教程:http://www.ruanyifeng.com/blog/2017/04/memory-leak.html

一、什麽是內存泄漏?

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

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

技術分享圖片

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

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


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

// Do something with buffer

free(buffer);

上面是 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]引用,這塊內存就可以被垃圾回收機制釋放了。


let 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"

上面代碼中,先新建一個 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 }

> let wm = new WeakMap();
undefined

> let b = new Object();
undefined

> global.gc();
undefined

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

// 在 WeakMap 中添加一個鍵值對,
// 鍵名為對象 b,鍵值為一個 5*1024*1024 的數組  
> wm.set(b, new Array(5*1024*1024));
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 中的那個長度為 5*1024*1024 的數組被銷毀了
> process.memoryUsage(); 
{ rss: 20639744,
  heapTotal: 8425472,
  heapUsed: 3979792,
  external: 8956 }

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

六、參考鏈接

  • Simple Guide to Finding a JavaScript Memory Leak in Node.js
  • Understanding Garbage Collection and hunting Memory Leaks in Node.js
  • Debugging Memory Leaks in Node.js Applications

JavaScript學習總結(二十三)——JavaScript 內存泄漏教程