Javascript記憶體洩漏

老毛發表於2013-04-05

1.什麼是記憶體洩漏?

記憶體洩漏是指分配給應用的記憶體不能被重新分配,即使在記憶體已經不被使用的時候。正常情況下,垃圾回收器在DOM元素和event處理器不被引用或訪問的時候回收它們。但是,IE的早些版本(IE7和之前)中記憶體洩漏是很容易出現的,因為記憶體管理器不能正確理解Javascript生命週期而且在週期被打破(可以通過賦值為null實現)前不會回收記憶體。

2.為什麼你需要注意它?

在大型Web應用程式中記憶體洩漏是一種常見的意外程式設計錯誤。記憶體洩漏會降低Web應用程式的效能,直到浪費的記憶體超過了系統所能分配的,應用程式將不能使用。作為一Web開發者,開發一個滿足功能要求的應用程式只是第一步,效能要求和Web應用程式的成功是同樣重要的,更何況它可能會導致應用程式錯誤或瀏覽器崩潰。

3.Javascript中出現記憶體洩漏的主要原因是什麼?

1)迴圈引用

一個很簡單的例子:一個DOM物件被一個Javascript物件引用,與此同時又引用同一個或其它的Javascript物件,這個DOM物件可能會引發記憶體洩漏。這個DOM物件的引用將不會在指令碼停止的時候被垃圾回收器回收。要想破壞迴圈引用,引用DOM元素的物件或DOM物件的引用需要被賦值為null。

2)Javascript閉包

因為Javascript範圍的限制,許多實現依賴Javascript不包,請檢視我的前面的文章JavaScript Scope and Closure如果你想了解更多閉包方面的問題。

閉包可以導致記憶體洩漏是因為內部方法保持一個對外部方法變數的引用,所以儘管方法返回了內部方法還可以繼續訪問在外部方法中定義的私有變數。對Javascript程式設計師來說最好的做法是在頁面過載前斷開所有的事件處理器。

3)DOM插入順序

當2個不同範圍的 DOM 物件連新增到一起的時候一個臨時的物件會被建立。這個DOM物件改變範圍到document時,那個臨時物件就沒用了。也就是說, DOM 物件應該按照從當前頁面存在的最上面的 DOM 元素開始往下直到剩下的 DOM 元素的順序新增,這樣它們就總是有同樣的範圍,不會產生臨時物件。

4)如何檢測?

記憶體洩漏對開發者來說一般很難檢測因為它們是由一些大量程式碼中的意外的錯誤引起的,但它在系統記憶體不足前並不影響程式的功能。這就是為什麼會有人在很長時間的測試期中收集應用程式效能指標來測試效能。

最簡單的檢測記憶體洩漏的方式是用工作管理員檢查記憶體使用情況。在Chrome瀏覽器的新選項卡中開啟應用並檢視記憶體使用量是不是越來越多。還有其他的除錯工具提供記憶體監視器,比如Chrome開發者工具。這是谷歌開者這網站中的堆分析的特性的教程。

Javascript記憶體洩漏

參考:

1. http://javascript.crockford.com/memory/leak.html

2. http://msdn.microsoft.com/en-us/library/Bb250448

3. http://www.ibm.com/developerworks/web/library/wa-memleak/

 

原文連結/OsChina.NET編譯

/*   更新:2012-5-5 23:27    */

/*  此文摘要釋出至微博後,@寒冬winter  論評說: “ 其實1和2是同一個原因,而且此文漏掉一種情況,老朽很久前寫過一篇舊文 ”,如下。 */

 

什麼是記憶體洩露

記憶體洩露是指一塊被分配的記憶體既不能使用,又不能回收,直到瀏覽器程式結束。在C++中,因為是手動管理記憶體,記憶體洩露是經常出現的事情。而現在流行的C#和Java等語言採用了自動垃圾回收方法管理記憶體,正常使用的情況下幾乎不會發生記憶體洩露。瀏覽器中也是採用自動垃圾回收方法管理記憶體,但由於瀏覽器垃圾回收方法有bug,會產生記憶體洩露。

記憶體洩露Quick View

不同的瀏覽器中存在各種記憶體洩露方式,目前發現的主要是這樣幾種:

1.  迴圈引用

已經確認存在洩漏的瀏覽器:IE6.0 FF2.0

含有DOM物件的迴圈引用將導致大部分當前主流瀏覽器記憶體洩露 這裡有兩個簡單的概念

引用:a.屬性=b,a就引用了b

迴圈引用:簡單來說假如a引用了b,b又引用了a,a和b就構成了迴圈引用。

a和b迴圈引用:

a迴圈引用自己:

迴圈引用很常見且大部分情況下是無害的,但當參與迴圈引用的物件中有DOM物件或者ActiveX物件時,迴圈引用將導致記憶體洩露。我們把例子中的任何一個new Object替換成document.getElementById或者document.createElement就會發生記憶體洩露了。
儘管這看起來非常容易理解,但是因為有closure的參與而使事情變得複雜,有些closure導致的迴圈引用很難被察覺。下面是一個非常常見的動態繫結事件:

這個bindEvent執行時100%會發生記憶體洩露,Someone 可能會問,哪裡出現了迴圈引用? 關於closure和scope chain參與的迴圈引用比較複雜,此處暫不深入討論。有一個簡單的判斷方式:函式將間接引用所有它能訪問的物件。obj.onclick這個函式中 可以訪問外部的變數obj 所以他引用了obj,而obj又引用了它,因此這個事件繫結將會造成記憶體洩露。在IBM的文章中介紹了2種方式解決類似的問題一個是obj=null,另一個是把onclick的函式寫在bindEvent外,重複人家的我就不說了。簡單貼下程式碼:

這兩個方法都打斷了迴圈引用,可以解決問題,但是似乎對程式碼表達能力造成了一定破壞,假設有這麼一個問題:

好了,這下兩種辦法都不行了,假如我把函式寫外面去,var0肯定訪問不了,假如我把obj弄成null,還怎麼return它呢?這並不是空想的需要,這實際 上是一個用JS定製DOM控制元件的簡單抽象:建立DOM元素、設定私有屬性、繫結事件。所以,我們必須update一下兩個方法。首先,方法1,為了讓函式 能訪問某些變數,我們可以通過一個Builder函式來訂製onclick的外部閉包:

第二個辦法,這個來自51js的chpn同學,讓obj=null在return 之後執行!!

2.  某些DOM操作

這是IE系列的特有問題 簡單的來說就是在向不在DOM樹上的DOM元素appendChild,可能會發生記憶體洩露(只是可能,具體原因不明,似乎十分複雜,下面例子中去掉onClick也可以避免洩露)。所以appendChild的順序可能影響記憶體洩露,來自微軟的例子:

而在IE7中,貌似為了改善記憶體洩露,IE7採用了極端的解決方案:離開頁面時回收所有DOM樹上的元素,其它一概不管。但是這不僅沒起到任何作用,反而 使問題變得更加複雜。對這類問題,除了自覺一點繞開這些噁心的東西,多用innerHTML這種無用的建議之外。我想可以通過覆蓋 document.createElement來略為改善:

首先我們定義一個看不見的元素當作垃圾箱,所有新建立的元素都扔進垃圾箱裡,這樣保證了所有DOM元素都在DOM樹上,IE7就可以正確回收了,另一方面也能避免所謂的”appendChild順序不對導致記憶體洩露”。

3.  自動型別裝箱轉換

別不相信,下面的程式碼在ie系列中會導致記憶體洩露

s本身是一個string而非object,它沒有length屬性,所以當訪問length時,JS引擎會自動建立一個臨時String物件封裝s,而這個物件一定會洩露。這個bug匪夷所思,所幸解決起來相當容易,記得所有值型別做.運算之前先顯式轉換一下:

 

參考

  1. Understanding and Solving Internet Explorer Leak Patterns(中文版)
  2. Memory leak patterns in JavaScript(中文版)
  3. 51js的一則討論 

 

寒冬的原文:瀏覽器中的記憶體洩露

相關文章